Skip to content

Commit 131f481

Browse files
Add commonmark-spec markdown support with markwon library
Also adds MarkdownUtitls to provide various utils for markdown processing.
1 parent f393e9b commit 131f481

File tree

2 files changed

+199
-0
lines changed

2 files changed

+199
-0
lines changed

app/build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ plugins {
22
id "com.android.application"
33
}
44

5+
ext.markwon_version='4.6.2'
6+
57
android {
68
compileSdkVersion project.properties.compileSdkVersion.toInteger()
79
ndkVersion project.properties.ndkVersion
@@ -14,6 +16,10 @@ android {
1416
implementation 'androidx.preference:preference:1.1.1'
1517
implementation "androidx.viewpager:viewpager:1.0.0"
1618
implementation 'com.google.guava:guava:24.1-jre'
19+
implementation "io.noties.markwon:core:$markwon_version"
20+
implementation "io.noties.markwon:ext-strikethrough:$markwon_version"
21+
implementation "io.noties.markwon:linkify:$markwon_version"
22+
implementation "io.noties.markwon:recycler:$markwon_version"
1723
implementation project(":terminal-view")
1824
}
1925

@@ -89,6 +95,8 @@ android {
8995
}
9096

9197
dependencies {
98+
implementation 'androidx.appcompat:appcompat:1.2.0'
99+
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
92100
testImplementation 'junit:junit:4.13.1'
93101
testImplementation 'org.robolectric:robolectric:4.4'
94102
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package com.termux.app.utils;
2+
3+
import android.content.Context;
4+
import android.graphics.Typeface;
5+
import android.text.Spanned;
6+
import android.text.style.AbsoluteSizeSpan;
7+
import android.text.style.BackgroundColorSpan;
8+
import android.text.style.BulletSpan;
9+
import android.text.style.QuoteSpan;
10+
import android.text.style.StrikethroughSpan;
11+
import android.text.style.StyleSpan;
12+
import android.text.style.TypefaceSpan;
13+
import android.text.util.Linkify;
14+
15+
import androidx.annotation.NonNull;
16+
import androidx.core.content.ContextCompat;
17+
18+
import com.google.common.base.Strings;
19+
import com.termux.R;
20+
21+
import org.commonmark.ext.gfm.strikethrough.Strikethrough;
22+
import org.commonmark.node.BlockQuote;
23+
import org.commonmark.node.Code;
24+
import org.commonmark.node.Emphasis;
25+
import org.commonmark.node.FencedCodeBlock;
26+
import org.commonmark.node.ListItem;
27+
import org.commonmark.node.StrongEmphasis;
28+
29+
import java.util.regex.Matcher;
30+
import java.util.regex.Pattern;
31+
32+
import io.noties.markwon.AbstractMarkwonPlugin;
33+
import io.noties.markwon.Markwon;
34+
import io.noties.markwon.MarkwonSpansFactory;
35+
import io.noties.markwon.MarkwonVisitor;
36+
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
37+
import io.noties.markwon.linkify.LinkifyPlugin;
38+
39+
public class MarkdownUtils {
40+
41+
public static String backtick = "`";
42+
public static Pattern backticksPattern = Pattern.compile("(" + backtick + "+)");
43+
44+
/**
45+
* Get the markdown code {@link String} for a {@link String}. This ensures all backticks "`" are
46+
* properly escaped so that markdown does not break.
47+
*
48+
* @param string The {@link String} to convert.
49+
* @param codeBlock If the {@link String} is to be converted to a code block or inline code.
50+
* @return Returns the markdown code {@link String}.
51+
*/
52+
public static String getMarkdownCodeForString(String string, boolean codeBlock) {
53+
if(string == null) return null;
54+
if(string.isEmpty()) return "";
55+
56+
int maxConsecutiveBackTicksCount = getMaxConsecutiveBackTicksCount(string);
57+
58+
// markdown requires surrounding backticks count to be at least one more than the count
59+
// of consecutive ticks in the string itself
60+
int backticksCountToUse;
61+
if(codeBlock)
62+
backticksCountToUse = maxConsecutiveBackTicksCount + 3;
63+
else
64+
backticksCountToUse = maxConsecutiveBackTicksCount + 1;
65+
66+
// create a string with n backticks where n==backticksCountToUse
67+
String backticksToUse = Strings.repeat(backtick, backticksCountToUse);
68+
69+
if(codeBlock)
70+
return backticksToUse + "\n" + string + "\n" + backticksToUse;
71+
else {
72+
// add a space to any prefixed or suffixed backtick characters
73+
if(string.startsWith(backtick))
74+
string = " " + string;
75+
if(string.endsWith(backtick))
76+
string = string + " ";
77+
78+
return backticksToUse + string + backticksToUse;
79+
}
80+
}
81+
82+
/**
83+
* Get the max consecutive backticks "`" in a {@link String}.
84+
*
85+
* @param string The {@link String} to check.
86+
* @return Returns the max consecutive backticks count.
87+
*/
88+
public static int getMaxConsecutiveBackTicksCount(String string) {
89+
if(string == null || string.isEmpty()) return 0;
90+
91+
int maxCount = 0;
92+
int matchCount;
93+
94+
Matcher matcher = backticksPattern.matcher(string);
95+
while(matcher.find()) {
96+
matchCount = matcher.group(1).length();
97+
if(matchCount > maxCount)
98+
maxCount = matchCount;
99+
}
100+
101+
return maxCount;
102+
}
103+
104+
105+
106+
public static String getSingleLineMarkdownStringEntry(String label, Object object, String def) {
107+
if (object != null)
108+
return "**" + label + "**: " + getMarkdownCodeForString(object.toString(), false) + " ";
109+
else
110+
return "**" + label + "**: " + def + " ";
111+
}
112+
113+
public static String getMultiLineMarkdownStringEntry(String label, Object object, String def) {
114+
if (object != null)
115+
return "**" + label + "**:\n" + getMarkdownCodeForString(object.toString(), true) + "\n";
116+
else
117+
return "**" + label + "**: " + def + "\n";
118+
}
119+
120+
121+
/** Check following for more info:
122+
* https://github.com/noties/Markwon/tree/v4.6.2/app-sample
123+
* https://noties.io/Markwon/docs/v4/recycler/
124+
* https://github.com/noties/Markwon/blob/v4.6.2/app-sample/src/main/java/io/noties/markwon/app/readme/ReadMeActivity.kt
125+
*/
126+
public static Markwon getRecyclerMarkwonBuilder(Context context) {
127+
return Markwon.builder(context)
128+
.usePlugin(LinkifyPlugin.create(Linkify.EMAIL_ADDRESSES | Linkify.WEB_URLS))
129+
.usePlugin(new AbstractMarkwonPlugin() {
130+
@Override
131+
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
132+
builder.on(FencedCodeBlock.class, (visitor, fencedCodeBlock) -> {
133+
// we actually won't be applying code spans here, as our custom xml view will
134+
// draw background and apply mono typeface
135+
//
136+
// NB the `trim` operation on literal (as code will have a new line at the end)
137+
final CharSequence code = visitor.configuration()
138+
.syntaxHighlight()
139+
.highlight(fencedCodeBlock.getInfo(), fencedCodeBlock.getLiteral().trim());
140+
visitor.builder().append(code);
141+
});
142+
}
143+
144+
@Override
145+
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
146+
builder
147+
// set color for inline code
148+
.setFactory(Code.class, (configuration, props) -> new Object[]{
149+
new BackgroundColorSpan(ContextCompat.getColor(context, R.color.background_markdown_code_inline)),
150+
});
151+
}
152+
})
153+
.build();
154+
}
155+
156+
/** Check following for more info:
157+
* https://github.com/noties/Markwon/tree/v4.6.2/app-sample
158+
* https://github.com/noties/Markwon/blob/v4.6.2/app-sample/src/main/java/io/noties/markwon/app/samples/notification/NotificationSample.java
159+
*/
160+
public static Markwon getSpannedMarkwonBuilder(Context context) {
161+
return Markwon.builder(context)
162+
.usePlugin(StrikethroughPlugin.create())
163+
.usePlugin(new AbstractMarkwonPlugin() {
164+
@Override
165+
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
166+
builder
167+
.setFactory(Emphasis.class, (configuration, props) -> new StyleSpan(Typeface.ITALIC))
168+
.setFactory(StrongEmphasis.class, (configuration, props) -> new StyleSpan(Typeface.BOLD))
169+
.setFactory(BlockQuote.class, (configuration, props) -> new QuoteSpan())
170+
.setFactory(Strikethrough.class, (configuration, props) -> new StrikethroughSpan())
171+
// NB! notification does not handle background color
172+
.setFactory(Code.class, (configuration, props) -> new Object[]{
173+
new BackgroundColorSpan(ContextCompat.getColor(context, R.color.background_markdown_code_inline)),
174+
new TypefaceSpan("monospace"),
175+
new AbsoluteSizeSpan(8)
176+
})
177+
// NB! both ordered and bullet list items
178+
.setFactory(ListItem.class, (configuration, props) -> new BulletSpan());
179+
}
180+
})
181+
.build();
182+
}
183+
184+
public static Spanned getSpannedMarkdownText(Context context, String string) {
185+
186+
final Markwon markwon = getSpannedMarkwonBuilder(context);
187+
188+
return markwon.toMarkdown(string);
189+
}
190+
191+
}

0 commit comments

Comments
 (0)