2021-04-07 06:31:30 +00:00
|
|
|
package com.termux.shared.markdown;
|
2021-03-23 21:30:20 +00:00
|
|
|
|
|
|
|
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;
|
2021-04-07 06:31:30 +00:00
|
|
|
import com.termux.shared.R;
|
2021-03-23 21:30:20 +00:00
|
|
|
|
|
|
|
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 {
|
|
|
|
|
2021-04-07 06:31:30 +00:00
|
|
|
public static final String backtick = "`";
|
|
|
|
public static final Pattern backticksPattern = Pattern.compile("(" + backtick + "+)");
|
2021-03-23 21:30:20 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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";
|
|
|
|
}
|
|
|
|
|
Implement crash handler and reporting
Now whenever the Termux app crashes, the crash report (stacktrace, app and device info) will be logged to ~/crash_log.md file. When the user will reopen the app, a notification will be shown which when clicked will show the crash report content in the ReportActivity. The activity will have important links like email, reddit, github issues of termux app and packages at which the user can optionally report an issue if necessary after copying the crash report text. The ~/crash_log.md file will be moved to ~/crash_log-backup.md so that a notification is not shown again on next startup and can be viewed again via SAF, etc.
This will allow reports for bugs that are submitted to have complete and useful info, specially in markdown format, making lives of devs a tad bit easier. Also more bugs that are rare might be submitted since users will have the info to report with and know where to report at.
ToDo:
- The TermuxConstants.TERMUX_SUPPORT_EMAIL_URL needs to be updated with a valid support email once its set up. The TermuxUtils.getReportIssueMarkdownString() function currently also has "email" lines commented out which will need to be uncommented.
- Currently, crashes will only be handled for the main app thread, other threads will have to manually hooked into where necessary.
2021-04-06 11:15:00 +00:00
|
|
|
public static String getLinkMarkdownString(String label, Object object) {
|
|
|
|
if (object != null)
|
|
|
|
return "[" + label + "](" + object + ")";
|
|
|
|
else
|
|
|
|
return label;
|
|
|
|
}
|
|
|
|
|
2021-03-23 21:30:20 +00:00
|
|
|
|
|
|
|
/** 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);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|