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