termux-app/termux-shared/src/main/java/com/termux/shared/markdown/MarkdownUtils.java

199 lines
8.1 KiB
Java

package com.termux.shared.markdown;
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.shared.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 final String backtick = "`";
public static final 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";
}
public static String getLinkMarkdownString(String label, Object object) {
if (object != null)
return "[" + label + "](" + object + ")";
else
return label;
}
/** 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);
}
}