mirror of
https://github.com/fankes/termux-app.git
synced 2025-09-06 02:35:19 +08:00
Add commonmark-spec markdown support with markwon library
Also adds MarkdownUtitls to provide various utils for markdown processing.
This commit is contained in:
@@ -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'
|
||||
}
|
||||
|
191
app/src/main/java/com/termux/app/utils/MarkdownUtils.java
Normal file
191
app/src/main/java/com/termux/app/utils/MarkdownUtils.java
Normal file
@@ -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);
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user