From 131f481750dcea638371ac84f31fb0ac494c3f2c Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Wed, 24 Mar 2021 02:30:20 +0500 Subject: [PATCH] Add commonmark-spec markdown support with markwon library Also adds MarkdownUtitls to provide various utils for markdown processing. --- app/build.gradle | 8 + .../com/termux/app/utils/MarkdownUtils.java | 191 ++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 app/src/main/java/com/termux/app/utils/MarkdownUtils.java diff --git a/app/build.gradle b/app/build.gradle index 0d88b0a4..22bfe3ee 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,6 +2,8 @@ plugins { id "com.android.application" } +ext.markwon_version='4.6.2' + android { compileSdkVersion project.properties.compileSdkVersion.toInteger() ndkVersion project.properties.ndkVersion @@ -14,6 +16,10 @@ android { implementation 'androidx.preference:preference:1.1.1' implementation "androidx.viewpager:viewpager:1.0.0" implementation 'com.google.guava:guava:24.1-jre' + implementation "io.noties.markwon:core:$markwon_version" + implementation "io.noties.markwon:ext-strikethrough:$markwon_version" + implementation "io.noties.markwon:linkify:$markwon_version" + implementation "io.noties.markwon:recycler:$markwon_version" implementation project(":terminal-view") } @@ -89,6 +95,8 @@ android { } dependencies { + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' testImplementation 'junit:junit:4.13.1' testImplementation 'org.robolectric:robolectric:4.4' } diff --git a/app/src/main/java/com/termux/app/utils/MarkdownUtils.java b/app/src/main/java/com/termux/app/utils/MarkdownUtils.java new file mode 100644 index 00000000..712b01b2 --- /dev/null +++ b/app/src/main/java/com/termux/app/utils/MarkdownUtils.java @@ -0,0 +1,191 @@ +package com.termux.app.utils; + +import android.content.Context; +import android.graphics.Typeface; +import android.text.Spanned; +import android.text.style.AbsoluteSizeSpan; +import android.text.style.BackgroundColorSpan; +import android.text.style.BulletSpan; +import android.text.style.QuoteSpan; +import android.text.style.StrikethroughSpan; +import android.text.style.StyleSpan; +import android.text.style.TypefaceSpan; +import android.text.util.Linkify; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; + +import com.google.common.base.Strings; +import com.termux.R; + +import org.commonmark.ext.gfm.strikethrough.Strikethrough; +import org.commonmark.node.BlockQuote; +import org.commonmark.node.Code; +import org.commonmark.node.Emphasis; +import org.commonmark.node.FencedCodeBlock; +import org.commonmark.node.ListItem; +import org.commonmark.node.StrongEmphasis; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.noties.markwon.AbstractMarkwonPlugin; +import io.noties.markwon.Markwon; +import io.noties.markwon.MarkwonSpansFactory; +import io.noties.markwon.MarkwonVisitor; +import io.noties.markwon.ext.strikethrough.StrikethroughPlugin; +import io.noties.markwon.linkify.LinkifyPlugin; + +public class MarkdownUtils { + + public static String backtick = "`"; + public static Pattern backticksPattern = Pattern.compile("(" + backtick + "+)"); + + /** + * Get the markdown code {@link String} for a {@link String}. This ensures all backticks "`" are + * properly escaped so that markdown does not break. + * + * @param string The {@link String} to convert. + * @param codeBlock If the {@link String} is to be converted to a code block or inline code. + * @return Returns the markdown code {@link String}. + */ + public static String getMarkdownCodeForString(String string, boolean codeBlock) { + if(string == null) return null; + if(string.isEmpty()) return ""; + + int maxConsecutiveBackTicksCount = getMaxConsecutiveBackTicksCount(string); + + // markdown requires surrounding backticks count to be at least one more than the count + // of consecutive ticks in the string itself + int backticksCountToUse; + if(codeBlock) + backticksCountToUse = maxConsecutiveBackTicksCount + 3; + else + backticksCountToUse = maxConsecutiveBackTicksCount + 1; + + // create a string with n backticks where n==backticksCountToUse + String backticksToUse = Strings.repeat(backtick, backticksCountToUse); + + if(codeBlock) + return backticksToUse + "\n" + string + "\n" + backticksToUse; + else { + // add a space to any prefixed or suffixed backtick characters + if(string.startsWith(backtick)) + string = " " + string; + if(string.endsWith(backtick)) + string = string + " "; + + return backticksToUse + string + backticksToUse; + } + } + + /** + * Get the max consecutive backticks "`" in a {@link String}. + * + * @param string The {@link String} to check. + * @return Returns the max consecutive backticks count. + */ + public static int getMaxConsecutiveBackTicksCount(String string) { + if(string == null || string.isEmpty()) return 0; + + int maxCount = 0; + int matchCount; + + Matcher matcher = backticksPattern.matcher(string); + while(matcher.find()) { + matchCount = matcher.group(1).length(); + if(matchCount > maxCount) + maxCount = matchCount; + } + + return maxCount; + } + + + + public static String getSingleLineMarkdownStringEntry(String label, Object object, String def) { + if (object != null) + return "**" + label + "**: " + getMarkdownCodeForString(object.toString(), false) + " "; + else + return "**" + label + "**: " + def + " "; + } + + public static String getMultiLineMarkdownStringEntry(String label, Object object, String def) { + if (object != null) + return "**" + label + "**:\n" + getMarkdownCodeForString(object.toString(), true) + "\n"; + else + return "**" + label + "**: " + def + "\n"; + } + + + /** Check following for more info: + * https://github.com/noties/Markwon/tree/v4.6.2/app-sample + * https://noties.io/Markwon/docs/v4/recycler/ + * https://github.com/noties/Markwon/blob/v4.6.2/app-sample/src/main/java/io/noties/markwon/app/readme/ReadMeActivity.kt + */ + public static Markwon getRecyclerMarkwonBuilder(Context context) { + return Markwon.builder(context) + .usePlugin(LinkifyPlugin.create(Linkify.EMAIL_ADDRESSES | Linkify.WEB_URLS)) + .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 xml 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); + }); + } + + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + builder + // set color for inline code + .setFactory(Code.class, (configuration, props) -> new Object[]{ + new BackgroundColorSpan(ContextCompat.getColor(context, R.color.background_markdown_code_inline)), + }); + } + }) + .build(); + } + + /** Check following for more info: + * https://github.com/noties/Markwon/tree/v4.6.2/app-sample + * https://github.com/noties/Markwon/blob/v4.6.2/app-sample/src/main/java/io/noties/markwon/app/samples/notification/NotificationSample.java + */ + public static Markwon getSpannedMarkwonBuilder(Context context) { + return Markwon.builder(context) + .usePlugin(StrikethroughPlugin.create()) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + builder + .setFactory(Emphasis.class, (configuration, props) -> new StyleSpan(Typeface.ITALIC)) + .setFactory(StrongEmphasis.class, (configuration, props) -> new StyleSpan(Typeface.BOLD)) + .setFactory(BlockQuote.class, (configuration, props) -> new QuoteSpan()) + .setFactory(Strikethrough.class, (configuration, props) -> new StrikethroughSpan()) + // NB! notification does not handle background color + .setFactory(Code.class, (configuration, props) -> new Object[]{ + new BackgroundColorSpan(ContextCompat.getColor(context, R.color.background_markdown_code_inline)), + new TypefaceSpan("monospace"), + new AbsoluteSizeSpan(8) + }) + // NB! both ordered and bullet list items + .setFactory(ListItem.class, (configuration, props) -> new BulletSpan()); + } + }) + .build(); + } + + public static Spanned getSpannedMarkdownText(Context context, String string) { + + final Markwon markwon = getSpannedMarkwonBuilder(context); + + return markwon.toMarkdown(string); + } + +}