Awesome Open Source
Awesome Open Source

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

Override MarkyMark styles where nescessary.

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

All parent styles

  • MarkyMarkCode
  • MarkyMarkHeader
  • MarkyMarkHeader1
  • MarkyMarkHeader2
  • MarkyMarkHeader3
  • MarkyMarkHeader4
  • MarkyMarkHeader5
  • MarkyMarkHeader6
  • MarkyMarkHeaderHorizontalRule // horizontal lines
  • MarkyMarkImage
  • MarkyMarkList
  • MarkyMarkParagraph
  • MarkyMarkQuote

Next use MarkyMark to parse Markdown and add the resulting list of Views to a layout.

val markyMark = MarkyMarkAndroid.getMarkyMark(this, ContentfulFlavor())
val views = markyMark.parseMarkDown("# Header\nParagraph etc")
for (view in views) {
     layout.addView(view)
}

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
)

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 -> € ©

Images


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

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

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:

  • Pass a list of Strings to an rule
  • The rule checks whether the first string conforms to that particular MarkDownItem
  • The rule also counts (if the first String conforms) how many lines belong to the item
  • The parser asks the rule how many lines belong to the item
  • The parser passes those lines to the rule to create the MarkDownItem
  • The parser removes those lines from the original list of Strings
  • Repeat

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, [email protected]

License


The MIT License (MIT)

Copyright (c) 2016-2018 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.
Related Awesome Lists
Top Programming Languages
Top Projects

Get A Weekly Email With Trending Projects For These Topics
No Spam. Unsubscribe easily at any time.
Java (392,336
Markdown (28,317
Block (21,252
Line (17,889
Inline (4,281
Contentful (1,036
Markdown Parser (333