Move-Agency / MarkyMark-Android

Markdown parser for Android
MIT License
29 stars 6 forks source link
android contentful markdown markdown-parser

MarkyMark

MarkyMark is a parser that converts markdown into native views. The way it looks is highly customizable and the supported markdown syntax is easy to extend.

Usage Android

Create an Android instance of MarkyMark that can convert Markdown to View's. A helper class is provided to create an Android instance.

val markyMark = MarkyMarkAndroid.getMarkyMark(this, ContentfulFlavor(), PicassoImageLoader())

There are 2 ways to use the MarkyMark instance.

You can use the MarkyMark instance to parse the Markdown into a list of View's and add them to (for example) a LinearLayout:

val linearLayout = findViewById<LinearLayout>(R.id.linearlayout)
val views = markyMark.parseMarkdown("# Header\nParagraph etc")
for (view in views) {
     layout.addView(view)
}

Or use the provided MarkyMarkView to do it for you.

val markyMarkView = findViewById<MarkyMarkView>(R.id.markymarkview)
markyMarkView.setMarkyMark(markyMark)
markyMarkView.setMarkdown("# Header\nParagraph etc")

Styling

To style your Markdown content you can override MarkyMark styles where necessary.

<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>

    <item name="MarkyMarkTheme">@style/MarkdownStyle</item>  
 </style>

 <!-- Theme used by MarkyMark -->
 <style name="MarkdownStyle" parent="MarkyMarkStyle">
    <item name="android:lineSpacingExtra">4dp</item>
    <item name="android:lineSpacingMultiplier">1</item>
    <item name="MarkdownHeader4Style">@style/Header4</item>
</style>

<!-- Different color for H4 tags -->
<style name="Header4" parent="MarkyMarkHeader4">
    <item name="android:textColor">#52c222</item>
</style>

Available parent styles

If you want more customization it is also possible to provide your own custom inline/block items, or replace existing ones. More on how you can use these in Advanced Usage found below

val themedContext = ThemedContext(activity)

// These are some of the default ones, but you get the point
val displayItems = mutableListOf<DisplayItem<*,*,*>>().apply {
    add(HeaderDisplayItem(themedContext))
    add(ParagraphDisplayItem(themedContext))
}
val inlineDisplayItems = mutableListOf<InlineDisplayItem<*,*>>().apply {
    add(BoldInlineDisplayItem())
}
val markyMark = MarkyMarkAndroid.getMarkyMark(
                activity,
                ContentfulFlavor(),
                displayItems,
                inlineDisplayItems,
                PicassoImageLoader()
)

Loading images

You can use your favorite image loading library to load images. When creating a MarkyMark android instance an implementation of ImageLoader needs to be provided, an example using Picasso can be found here.

Advanced usage


Adding your own rules

As of now only a ContentfulFlavor is included, however it is entirely possible to create your own Markdown flavor and/or corresponding rules.

Adding a rule requires these steps

Extend the marker interface MarkdownItem

Create a MarkdownItem from which you can create a View later, so you'll want to have every piece of information needed in order to create said View

data class NewMarkdownItem(val content: String) : MarkdownItem

Extend DisplayItem<View, NewMarkdownItem, Spanned>

Create a DisplayItem that can handle your NewMarkdownItem and convert it into a View

class NewDisplayItem(val context: Context) : DisplayItem<View, NewMarkdownItem, Spanned> {

    override fun create(markdownItem: NewMarkdownItem, inlineConverter: InlineConverter<Spanned>) : View {
        return TextView(context).apply {
            text = inlineConverter.convert(markdownItem.content)
        }
    }
}

Now add this item to your ViewConverter before you initialize MarkyMark like shown at the top of this README

viewConverter.addMapping(NewDisplayItem(themedContext))

Extend Rule

Create a Rule that recognizes your new item and creates a corresponding MarkdownItem for it. Most new rules will just be single line, like headers, in that case your new rule can just extend RegexRule. Return your regular expression Pattern in the getRegex() method and return your new MarkdownItem in the toMarkdownItem(markdownLines: List<String>)

class NewRule : RegexRule {

    override fun getRegex() : Pattern = Pattern.compile("some regex")

    override fun toMarkdownItem(markdownLines: List<String>) : MarkdownItem {
        // In this case, since it is a single line rule
        // markdownLines will always be an list with one String
        return NewMarkdownItem(markdownLines.first())
    }
}

Adding the new rule to MarkyMark

You can add a new rule to your MarkyMark instance like this

// adding rule to MarkyMark instance
markyMark.addRule(NewRule())

Or create a new Flavor altogether

class OtherFlavor : Flavor {

    override fun getRules() : List<Rule> {
        return mutableListOf<Rule>().apply {
            // add all the rules
        }
    }

    override fun getInlineRules() : List<InlineRule> {
        return mutableListOf<InlineRule>().apply {
            // Add all the rules
        }
    }

    override fun getDefaultRule() = NewDefaultRule()
}

And pass it in the MarkyMark.Builder()

MarkyMark.Builder<View>().addFlavor(OtherFlavor()) // etc

Multi line blocks

For more complicated Rules that can detect multi-line blocks you'll want to extend Rule and you have override the conforms(final List<String> pMarkdownLines) method where you would return true if the first line is recognized as the start of your block, and false otherwise. However there is a catch, you have to set a global integer with the amount of lines there are in this block, which you have to return in the getLinesConsumed() method. This means that you have to count the amount of lines that belong to your block inside the conforms() method, which isn't desirable and should be refactored as soon as possible.

class NewRule : Rule {

    /** Start pattern of the block */
    private val startPattern = Pattern.compile("some regex")

    /** End pattern of the block */
    private val endPattern = Pattern.compile("some regex")

    /** The amount of lines of the block */
    private var lines : Int = 0

    override fun getLinesConsumed(): Int = lines

    override fun conforms(markdownLines: MutableList<String>): Boolean {
        if (!startPattern.matcher(markdownLines.first()).matches()) {
            return false
        }
        lines = 0
        for (line in markdownLines) {
            lines += 1
            if (endPattern.matcher(line).matches()) {
                return true
            }
        }
        return false
    }

    override fun toMarkdownItem(markdownLines: MutableList<String>): MarkdownItem = SomeMarkdownItem()
}

Inline rules

For detecting inline Markdown, like bold or italic strings, instead of extending RegexRule or Rule just extend InlineRule and return a MarkdownString instead of a MarkdownItem.

For example, a rule that would match %%some text%% would look like this

class PercentRule : InlineRule {

    override fun getRegex() : Pattern = Pattern.compile("%{2}(.+?)-{2}")

    override fun toMarkdownString(content: String) = PercentString(content, true)
}

Where PercentString would be an extension of MarkdownString

class PercentString(content: String, canHasChildItems: Boolean) : MarkdownString(content, canHasChildItems)

For inline Markdown, instead of extending DisplayItem<View, Foo, Spanned> you'd want to extend InlineDisplayItem<Spanned, PercentString>

class PercentInlineDisplayItem : InlineDisplayItem<Spanned, PercentString> {

    override fun create(inlineConverter: InlineConverter<Spanned>, markdownString: PercentString): Spanned {
        // return your Spannable String here
    }
}

And add them to MarkyMarks InlineConverter<Spanned> as explained above

inlineViewConverter.addMapping(PercentInlineDisplayItem())

Supported tags in Contentful Flavour

# Headers
---

# H1
## H2
### H3
#### H4
##### H5
###### H6
### __*bold & italic*__

# Lists
---

## Ordered list
1. Number 1
2. Number 2
3. Number 3
5. Number 4
  1. Nested 1
  2. Nested 2
6. Number 5 click [here](https://m2mobi.com)
7. Number 5

## Unordered list
- Item 1
- Item 2
- Item 3
  - Nested item 1
  - Nested item 2
    - Sub-nested item 1
    - Sub-nested item 2
      - Sub-sub-nested item 1
      - Sub-sub-nested item 2
       - Sub-sub-nested item 3 (with single space) and a very long piece of text

## Combo

1. Ordered 1
2. Ordered 2
- Unordered 1
- Unordered 2
  - __*Nested unordered 1 bold*__
  - Nested unordered 2
    1. Sub-nested ordered 1
    2. Sub-nested ordered 2
    - unordered
    - unordered

# Paragraphs

---
## Quotes
> Markdown is *awesome*
> Seriously..

## Links
[This is a test link](https://m2mobi.com)
Inline links are also possible, click [here](https://m2mobi.com)
Phone numbers as well [+06-12345678](tel:06-12345678)

## Code

`inline code`
```code block```

## Styled text
This is __bold__, this is *italic*, this is ~~striked out~~, this is everything __~~*combined*~~__.
Special html symbols: `&euro; &copy;` become -> &euro; &copy;

## Images
---

`![Alternate text](www.imageurl.com)`

Download


Add the Jitpack.io repository to your project root build.gradle file

allprojects {
    repositories {
        maven { url "https://jitpack.io" }
    }
}

Android MarkyMark with Contentful support

compile 'com.github.m2mobi.MarkyMark-Android:markymark-android:0.2.2' 
compile 'com.github.m2mobi.MarkyMark-Android:markymark-contentful:0.2.2' 

If you want to use MarkyMark outside of a Android project you might be interested in these pure Java modules

// Base
compile 'com.github.m2mobi.MarkyMark-Android:markymark-core:0.2.2' 
// Commons
compile 'com.github.m2mobi.MarkyMark-Android:markymark-commons:0.2.2' 

From which you can create MarkyMark like so

val viewConverter = Converter<View>().apply {
    addMapping(SomeItem())
}
val inlineConverter = InlineConverter<Spanned>().apply {
        addMapping(SomeInlineItem())
}

val markyMark = MarkyMark.Builder<View>()
    .addFlavor(SomeFlavor())
    .setConverter(viewConverter)
    .setInlineConverter(inlineConverter)
    .build()

Contributions


Contributions are encouraged! Create a PR against the Development branch and always run the tests before doing so. As of now only the markymark-contentful module has tests.

Starting points for contributions

Rule parsing

Like mentioned in Advanced Usage, the way rules are implemented now is:

Like previously mentioned, the rule counts the amount of lines needed for its item inside the conforms() method, something that is pretty unexpected when you aren't familiar with the code base. This process needs refactoring.

Table support

Implement support for tables.

Author


M2mobi, info@m2mobi.com

License


The MIT License (MIT)

Copyright (c) 2016-2020 M2mobi

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.