mirror of
https://github.com/nextcloud/notes-android.git
synced 2024-11-21 20:35:58 +03:00
Use markdown module from nextcloud-commons lib
Signed-off-by: Stefan Niedermann <info@niedermann.it>
This commit is contained in:
parent
fae4740719
commit
8f242754f6
59 changed files with 5 additions and 4688 deletions
|
@ -75,15 +75,15 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
// Markdown
|
||||
implementation project(path: ':markdown')
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
|
||||
// Nextcloud SSO
|
||||
implementation 'com.github.nextcloud:Android-SingleSignOn:0.5.6'
|
||||
implementation 'com.github.stefan-niedermann.nextcloud-commons:sso-glide:1.4.0'
|
||||
implementation 'com.github.stefan-niedermann.nextcloud-commons:exception:1.4.0'
|
||||
implementation 'com.github.stefan-niedermann.nextcloud-commons:sso-glide:1.5.0'
|
||||
implementation 'com.github.stefan-niedermann.nextcloud-commons:exception:1.5.0'
|
||||
implementation('com.github.stefan-niedermann.nextcloud-commons:markdown:1.5.0') {
|
||||
exclude group: 'org.jetbrains', module: 'annotations-java5'
|
||||
}
|
||||
implementation 'com.github.stefan-niedermann:android-commons:0.2.0'
|
||||
|
||||
// Glide
|
||||
|
|
1
markdown/.gitignore
vendored
1
markdown/.gitignore
vendored
|
@ -1 +0,0 @@
|
|||
/build
|
|
@ -1,67 +0,0 @@
|
|||
plugins {
|
||||
id 'com.android.library'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 31
|
||||
buildToolsVersion "31.0.0"
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 22
|
||||
targetSdkVersion 31
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles "consumer-rules.pro"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
coreLibraryDesugaringEnabled true
|
||||
sourceCompatibility JavaVersion.VERSION_11
|
||||
targetCompatibility JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
testOptions {
|
||||
unitTests {
|
||||
includeAndroidResources true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.github.nextcloud:Android-SingleSignOn:0.5.6'
|
||||
implementation 'com.github.stefan-niedermann:android-commons:0.2.0'
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
implementation 'androidx.lifecycle:lifecycle-livedata:2.3.1'
|
||||
|
||||
implementation 'org.jsoup:jsoup:1.14.3'
|
||||
|
||||
def markwonVersion = '4.6.2'
|
||||
implementation "io.noties.markwon:core:$markwonVersion"
|
||||
implementation "io.noties.markwon:editor:$markwonVersion"
|
||||
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
|
||||
implementation "io.noties.markwon:ext-tables:$markwonVersion"
|
||||
implementation "io.noties.markwon:ext-tasklist:$markwonVersion"
|
||||
implementation "io.noties.markwon:html:$markwonVersion"
|
||||
implementation "io.noties.markwon:image:$markwonVersion"
|
||||
implementation "io.noties.markwon:image-glide:$markwonVersion"
|
||||
implementation "io.noties.markwon:linkify:$markwonVersion"
|
||||
implementation "io.noties.markwon:simple-ext:$markwonVersion"
|
||||
implementation "io.noties.markwon:inline-parser:$markwonVersion"
|
||||
implementation("io.noties.markwon:syntax-highlight:$markwonVersion") {
|
||||
exclude group: 'org.jetbrains', module: 'annotations-java5'
|
||||
}
|
||||
implementation('io.noties:prism4j:2.0.0') {
|
||||
exclude group: 'org.jetbrains', module: 'annotations-java5'
|
||||
}
|
||||
annotationProcessor 'io.noties:prism4j-bundler:2.0.0'
|
||||
implementation 'org.jetbrains:annotations:22.0.0'
|
||||
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
|
||||
testImplementation 'androidx.test:core:1.4.0'
|
||||
testImplementation 'androidx.arch.core:core-testing:2.1.0'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
testImplementation 'org.mockito:mockito-core:4.0.0'
|
||||
testImplementation 'org.robolectric:robolectric:4.6.1'
|
||||
}
|
21
markdown/proguard-rules.pro
vendored
21
markdown/proguard-rules.pro
vendored
|
@ -1,21 +0,0 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="it.niedermann.android.markdown">
|
||||
|
||||
</manifest>
|
|
@ -1,80 +0,0 @@
|
|||
package it.niedermann.android.markdown;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.Html;
|
||||
import android.text.style.BulletSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.xml.sax.XMLReader;
|
||||
|
||||
import java.util.Stack;
|
||||
|
||||
/**
|
||||
* Adds <code>•</code> to unordered list items and a counter to ordered list items.
|
||||
* Call {@link #prepareTagHandling(String)}, so the default handler of {@link Html#fromHtml(String, int)} does not prevent the handling.
|
||||
*/
|
||||
public class ListTagHandler implements Html.TagHandler {
|
||||
|
||||
private static final String X_OL = "x-ol";
|
||||
private static final String X_UL = "x-ul";
|
||||
private static final String X_LI = "x-li";
|
||||
|
||||
private final Stack<String> parents = new Stack<>();
|
||||
private final Stack<Integer> listItemIndex = new Stack<>();
|
||||
|
||||
@Override
|
||||
public void handleTag(boolean opening, String tag, Editable output, XMLReader xmlReader) {
|
||||
if (X_OL.equals(tag)) {
|
||||
if (opening) {
|
||||
parents.push(X_OL);
|
||||
listItemIndex.push(1);
|
||||
} else {
|
||||
parents.pop();
|
||||
listItemIndex.pop();
|
||||
}
|
||||
} else if (X_UL.equals(tag)) {
|
||||
if (opening) {
|
||||
parents.push(X_UL);
|
||||
} else {
|
||||
parents.pop();
|
||||
}
|
||||
} else if (X_LI.equals(tag)) {
|
||||
if (X_OL.equals(parents.peek())) {
|
||||
if (opening) {
|
||||
output.append("\n");
|
||||
for (int nestingLevel = 1; nestingLevel < parents.size(); nestingLevel++) {
|
||||
output.append("\t\t");
|
||||
}
|
||||
output.append(String.valueOf(listItemIndex.peek())).append(". ");
|
||||
listItemIndex.push(listItemIndex.pop() + 1);
|
||||
}
|
||||
} else if (X_UL.equals(parents.peek())) {
|
||||
if (opening) {
|
||||
output.append("\n");
|
||||
for (int nestingLevel = 1; nestingLevel < parents.size(); nestingLevel++) {
|
||||
output.append("\t\t");
|
||||
}
|
||||
output.append("•");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the list tags with custom tags to prevent them being handeled by {@link HtmlCompat}.
|
||||
* Otherwise, all <code>li</code> tags will be replaced with {@link BulletSpan} which is not the
|
||||
* desired behavior of ordered list items.
|
||||
*/
|
||||
@NonNull
|
||||
public static String prepareTagHandling(@NonNull String html) {
|
||||
final Document document = Jsoup.parse(html);
|
||||
document.getElementsByTag("ol").tagName(X_OL);
|
||||
document.getElementsByTag("ul").tagName(X_UL);
|
||||
document.getElementsByTag("li").tagName(X_LI);
|
||||
return document.outerHtml();
|
||||
}
|
||||
}
|
|
@ -1,88 +0,0 @@
|
|||
package it.niedermann.android.markdown;
|
||||
|
||||
import android.text.style.URLSpan;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Can be used for editors and viewers as well.
|
||||
* Viewer can support basic edit features, like toggling checkboxes
|
||||
*/
|
||||
public interface MarkdownEditor {
|
||||
|
||||
String TAG = MarkdownEditor.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* The given {@link String} will be parsed and rendered
|
||||
*/
|
||||
void setMarkdownString(CharSequence text);
|
||||
|
||||
/**
|
||||
* The given {@link String} will be parsed and rendered and the {@param afterRender} will be called after the rendering finished
|
||||
*/
|
||||
void setMarkdownString(CharSequence text, @Nullable Runnable afterRender);
|
||||
|
||||
/**
|
||||
* Will replace all `@mention`s of Nextcloud users with the avatar and given display name.
|
||||
*
|
||||
* @param mentions {@link Map} of mentions, where the key is the user id and the value is the display name
|
||||
*/
|
||||
default void setMarkdownStringAndHighlightMentions(CharSequence text, @NonNull Map<String, String> mentions) {
|
||||
setMarkdownString(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the source {@link CharSequence} of the currently rendered markdown
|
||||
*/
|
||||
LiveData<CharSequence> getMarkdownString();
|
||||
|
||||
/**
|
||||
* Similar to {@link #getMarkdownString()} but without {@link LiveData}. Will remove previously set {@link Consumer}s.
|
||||
*
|
||||
* @param listener a {@link Consumer} which will receive the changed markdown string.
|
||||
*/
|
||||
void setMarkdownStringChangedListener(@Nullable Consumer<CharSequence> listener);
|
||||
|
||||
void setEnabled(boolean enabled);
|
||||
|
||||
/**
|
||||
* @param color which will be used for highlighting. See {@link #setSearchText(CharSequence)}
|
||||
*/
|
||||
default void setSearchColor(@ColorInt int color) {
|
||||
Log.w(TAG, "This feature is not supported by the currently used implementation.");
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@link #setSearchText(CharSequence, Integer)}
|
||||
*/
|
||||
default void setSearchText(@Nullable CharSequence searchText) {
|
||||
setSearchText(searchText, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlights the given {@param searchText} in the {@link MarkdownEditor}.
|
||||
*
|
||||
* @param searchText the term to highlight
|
||||
* @param current highlights the occurrence of the {@param searchText} at this position special
|
||||
*/
|
||||
default void setSearchText(@Nullable CharSequence searchText, @Nullable Integer current) {
|
||||
Log.w(TAG, "This feature is not supported by the currently used implementation.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Intercepts each click on a clickable element like {@link URLSpan}s
|
||||
*
|
||||
* @param callback Will be called on a click. When the {@param callback} returns <code>true</code>, the click will not be propagated further.
|
||||
*/
|
||||
default void registerOnLinkClickCallback(@NonNull Function<String, Boolean> callback) {
|
||||
Log.w(TAG, "This feature is not supported by the currently used implementation.");
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package it.niedermann.android.markdown;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import it.niedermann.android.markdown.markwon.MarkwonMarkdownEditor;
|
||||
|
||||
public class MarkdownEditorImpl extends MarkwonMarkdownEditor {
|
||||
public MarkdownEditorImpl(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public MarkdownEditorImpl(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public MarkdownEditorImpl(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
}
|
|
@ -1,529 +0,0 @@
|
|||
package it.niedermann.android.markdown;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Paint;
|
||||
import android.os.Build;
|
||||
import android.text.Editable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.QuoteSpan;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.widget.RemoteViews.RemoteView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
|
||||
import org.commonmark.parser.Parser;
|
||||
import org.commonmark.renderer.html.HtmlRenderer;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import it.niedermann.android.markdown.model.EListType;
|
||||
import it.niedermann.android.markdown.model.SearchSpan;
|
||||
|
||||
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
|
||||
public class MarkdownUtil {
|
||||
|
||||
private static final String TAG = MarkdownUtil.class.getSimpleName();
|
||||
|
||||
private static final Parser PARSER = Parser.builder().build();
|
||||
private static final HtmlRenderer RENDERER = HtmlRenderer.builder().softbreak("<br>").build();
|
||||
|
||||
private static final Pattern PATTERN_CODE_FENCE = Pattern.compile("^(`{3,})");
|
||||
private static final Pattern PATTERN_ORDERED_LIST_ITEM = Pattern.compile("^(\\d+).\\s.+$");
|
||||
private static final Pattern PATTERN_ORDERED_LIST_ITEM_EMPTY = Pattern.compile("^(\\d+).\\s$");
|
||||
private static final Pattern PATTERN_MARKDOWN_LINK = Pattern.compile("\\[(.+)?]\\(([^ ]+?)?( \"(.+)\")?\\)");
|
||||
|
||||
private static final String PATTERN_QUOTE_BOLD_PUNCTUATION = Pattern.quote("**");
|
||||
|
||||
private static final Optional<String> CHECKBOX_CHECKED_EMOJI = getCheckboxEmoji(true);
|
||||
private static final Optional<String> CHECKBOX_UNCHECKED_EMOJI = getCheckboxEmoji(false);
|
||||
|
||||
private MarkdownUtil() {
|
||||
// Util class
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link RemoteView}s have a limited subset of supported classes to maintain compatibility with many different launchers.
|
||||
* <p>
|
||||
* Since {@link Markwon} makes heavy use of custom spans, this won't look nice e. g. at app widgets, because they simply won't be rendered.
|
||||
* Therefore we currently use {@link HtmlCompat} to filter supported spans from the output of {@link HtmlRenderer} as an intermediate step.
|
||||
*/
|
||||
public static CharSequence renderForRemoteView(@NonNull Context context, @NonNull String content) {
|
||||
// Create HTML string from Markup
|
||||
final String html = RENDERER.render(PARSER.parse(replaceCheckboxesWithEmojis(content)));
|
||||
|
||||
// Create Spanned from HTML, with special handling for ordered list items
|
||||
final Spanned spanned = HtmlCompat.fromHtml(ListTagHandler.prepareTagHandling(html), 0, null, new ListTagHandler());
|
||||
|
||||
// Enhance colors and margins of the Spanned
|
||||
return customizeQuoteSpanAppearance(context, spanned, 5, 30);
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private static Spanned customizeQuoteSpanAppearance(@NonNull Context context, @NonNull Spanned input, int stripeWidth, int gapWidth) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
|
||||
return input;
|
||||
}
|
||||
final var ssb = new SpannableStringBuilder(input);
|
||||
final var originalQuoteSpans = ssb.getSpans(0, ssb.length(), QuoteSpan.class);
|
||||
@ColorInt final int colorBlockQuote = ContextCompat.getColor(context, R.color.block_quote);
|
||||
for (final var originalQuoteSpan : originalQuoteSpans) {
|
||||
final int start = ssb.getSpanStart(originalQuoteSpan);
|
||||
final int end = ssb.getSpanEnd(originalQuoteSpan);
|
||||
ssb.removeSpan(originalQuoteSpan);
|
||||
ssb.setSpan(new QuoteSpan(colorBlockQuote, stripeWidth, gapWidth), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
return ssb;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static String replaceCheckboxesWithEmojis(@NonNull String content) {
|
||||
return runForEachCheckbox(content, (line) -> {
|
||||
for (final var listType : EListType.values()) {
|
||||
if (CHECKBOX_CHECKED_EMOJI.isPresent()) {
|
||||
line = line.replace(listType.checkboxChecked, CHECKBOX_CHECKED_EMOJI.get());
|
||||
line = line.replace(listType.checkboxCheckedUpperCase, CHECKBOX_CHECKED_EMOJI.get());
|
||||
}
|
||||
if (CHECKBOX_UNCHECKED_EMOJI.isPresent()) {
|
||||
line = line.replace(listType.checkboxUnchecked, CHECKBOX_UNCHECKED_EMOJI.get());
|
||||
}
|
||||
}
|
||||
return line;
|
||||
});
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static Optional<String> getCheckboxEmoji(boolean checked) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
final String[] emojis;
|
||||
// Seriously what the fuck, Samsung?
|
||||
// https://emojipedia.org/ballot-box-with-x/
|
||||
if (Build.MANUFACTURER != null && Build.MANUFACTURER.toLowerCase(Locale.getDefault()).contains("samsung")) {
|
||||
emojis = checked
|
||||
? new String[]{"✅", "☑️", "✔️"}
|
||||
: new String[]{"❌", "\uD83D\uDD32️", "☐️"};
|
||||
} else {
|
||||
emojis = checked
|
||||
? new String[]{"☒", "✅", "☑️", "✔️"}
|
||||
: new String[]{"☐", "❌", "\uD83D\uDD32️", "☐️"};
|
||||
}
|
||||
final var paint = new Paint();
|
||||
for (String emoji : emojis) {
|
||||
if (paint.hasGlyph(emoji)) {
|
||||
return Optional.of(emoji);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the given {@param map} function for each line which contains a checkbox
|
||||
*/
|
||||
@NonNull
|
||||
private static String runForEachCheckbox(@NonNull String markdownString, @NonNull Function<String, String> map) {
|
||||
final String[] lines = markdownString.split("\n");
|
||||
boolean isInFencedCodeBlock = false;
|
||||
int fencedCodeBlockSigns = 0;
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
final var matcher = PATTERN_CODE_FENCE.matcher(lines[i]);
|
||||
if (matcher.find()) {
|
||||
final String fence = matcher.group(1);
|
||||
if (fence != null) {
|
||||
int currentFencedCodeBlockSigns = fence.length();
|
||||
if (isInFencedCodeBlock) {
|
||||
if (currentFencedCodeBlockSigns == fencedCodeBlockSigns) {
|
||||
isInFencedCodeBlock = false;
|
||||
fencedCodeBlockSigns = 0;
|
||||
}
|
||||
} else {
|
||||
isInFencedCodeBlock = true;
|
||||
fencedCodeBlockSigns = currentFencedCodeBlockSigns;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isInFencedCodeBlock) {
|
||||
if (lineStartsWithCheckbox(lines[i]) && lines[i].trim().length() > EListType.DASH.checkboxChecked.length()) {
|
||||
lines[i] = map.apply(lines[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return TextUtils.join("\n", lines);
|
||||
}
|
||||
|
||||
public static int getStartOfLine(@NonNull CharSequence s, int cursorPosition) {
|
||||
int startOfLine = cursorPosition;
|
||||
while (startOfLine > 0 && s.charAt(startOfLine - 1) != '\n') {
|
||||
startOfLine--;
|
||||
}
|
||||
return startOfLine;
|
||||
}
|
||||
|
||||
public static int getEndOfLine(@NonNull CharSequence s, int cursorPosition) {
|
||||
int nextLinebreak = s.toString().indexOf('\n', cursorPosition);
|
||||
if (nextLinebreak > -1) {
|
||||
return nextLinebreak;
|
||||
}
|
||||
return s.length();
|
||||
}
|
||||
|
||||
public static Optional<String> getListItemIfIsEmpty(@NonNull String line) {
|
||||
final String trimmedLine = line.trim();
|
||||
// TODO use Java 11 String::repeat
|
||||
final var builder = new StringBuilder();
|
||||
final int indention = line.indexOf(trimmedLine);
|
||||
for (int i = 0; i < indention; i++) {
|
||||
builder.append(" ");
|
||||
}
|
||||
for (final var listType : EListType.values()) {
|
||||
if (trimmedLine.equals(listType.checkboxUnchecked)) {
|
||||
return Optional.of(builder.append(listType.checkboxUncheckedWithTrailingSpace).toString());
|
||||
} else if (trimmedLine.equals(listType.listSymbol)) {
|
||||
return Optional.of(builder.append(listType.listSymbolWithTrailingSpace).toString());
|
||||
}
|
||||
}
|
||||
final var matcher = PATTERN_ORDERED_LIST_ITEM_EMPTY.matcher(line.substring(indention));
|
||||
if (matcher.find()) {
|
||||
return Optional.of(builder.append(matcher.group()).toString());
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public static CharSequence setCheckboxStatus(@NonNull String markdownString, int targetCheckboxIndex, boolean newCheckedState) {
|
||||
final String[] lines = markdownString.split("\n");
|
||||
int checkboxIndex = 0;
|
||||
boolean isInFencedCodeBlock = false;
|
||||
int fencedCodeBlockSigns = 0;
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
final var matcher = PATTERN_CODE_FENCE.matcher(lines[i]);
|
||||
if (matcher.find()) {
|
||||
final String fence = matcher.group(1);
|
||||
if (fence != null) {
|
||||
int currentFencedCodeBlockSigns = fence.length();
|
||||
if (isInFencedCodeBlock) {
|
||||
if (currentFencedCodeBlockSigns == fencedCodeBlockSigns) {
|
||||
isInFencedCodeBlock = false;
|
||||
fencedCodeBlockSigns = 0;
|
||||
}
|
||||
} else {
|
||||
isInFencedCodeBlock = true;
|
||||
fencedCodeBlockSigns = currentFencedCodeBlockSigns;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isInFencedCodeBlock) {
|
||||
if (lineStartsWithCheckbox(lines[i]) && lines[i].trim().length() > EListType.DASH.checkboxChecked.length()) {
|
||||
if (checkboxIndex == targetCheckboxIndex) {
|
||||
final int indexOfStartingBracket = lines[i].indexOf("[");
|
||||
final String toggledLine = lines[i].substring(0, indexOfStartingBracket + 1) +
|
||||
(newCheckedState ? 'x' : ' ') +
|
||||
lines[i].substring(indexOfStartingBracket + 2);
|
||||
lines[i] = toggledLine;
|
||||
break;
|
||||
}
|
||||
checkboxIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return TextUtils.join("\n", lines);
|
||||
}
|
||||
|
||||
public static boolean lineStartsWithCheckbox(@NonNull String line) {
|
||||
for (EListType listType : EListType.values()) {
|
||||
if (lineStartsWithCheckbox(line, listType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean lineStartsWithCheckbox(@NonNull String line, @NonNull EListType listType) {
|
||||
final String trimmedLine = line.trim();
|
||||
return (trimmedLine.startsWith(listType.checkboxUnchecked) || trimmedLine.startsWith(listType.checkboxChecked) || trimmedLine.startsWith(listType.checkboxCheckedUpperCase));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the number of the ordered list item if the line is an ordered list, otherwise -1.
|
||||
*/
|
||||
public static Optional<Integer> getOrderedListNumber(@NonNull String line) {
|
||||
final var matcher = PATTERN_ORDERED_LIST_ITEM.matcher(line);
|
||||
if (matcher.find()) {
|
||||
final String groupNumber = matcher.group(1);
|
||||
if (groupNumber != null) {
|
||||
try {
|
||||
return Optional.of(Integer.parseInt(groupNumber));
|
||||
} catch (NumberFormatException e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the {@param editable} and adds the given {@param punctuation} from
|
||||
* {@param selectionStart} to {@param selectionEnd} or removes the {@param punctuation} in case
|
||||
* it already is around the selected part.
|
||||
*
|
||||
* @return the new cursor position
|
||||
*/
|
||||
public static int togglePunctuation(@NonNull Editable editable, int selectionStart, int selectionEnd, @NonNull String punctuation) {
|
||||
final String initialString = editable.toString();
|
||||
if (selectionStart < 0 || selectionStart > initialString.length() || selectionEnd < 0 || selectionEnd > initialString.length()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// handle special case: italic (that damn thing will match like ANYTHING (regarding bold / bold+italic)....)
|
||||
final boolean isItalic = punctuation.length() == 1 && punctuation.charAt(0) == '*';
|
||||
if (isItalic) {
|
||||
final var result = handleItalicEdgeCase(editable, initialString, selectionStart, selectionEnd);
|
||||
// The result is only present if this actually was an edge case
|
||||
if (result.isPresent()) {
|
||||
return result.get();
|
||||
}
|
||||
}
|
||||
|
||||
// handle the simple cases
|
||||
final String wildcardRex = "([^" + punctuation.charAt(0) + "])+";
|
||||
final String punctuationRex = Pattern.quote(punctuation);
|
||||
final String pattern = isItalic
|
||||
// in this case let's make optional asterisks around it, so it wont match anything between two (bold+italic)s
|
||||
? "\\*?\\*?" + punctuationRex + wildcardRex + punctuationRex + "\\*?\\*?"
|
||||
: punctuationRex + wildcardRex + punctuationRex;
|
||||
final var searchPattern = Pattern.compile(pattern);
|
||||
int relevantStart = selectionStart - 2;
|
||||
relevantStart = Math.max(relevantStart, 0);
|
||||
int relevantEnd = selectionEnd + 2;
|
||||
relevantEnd = Math.min(relevantEnd, initialString.length());
|
||||
final Matcher matcher = searchPattern.matcher(initialString).region(relevantStart, relevantEnd);
|
||||
|
||||
// if the matcher matches, it's a remove
|
||||
if (matcher.find()) {
|
||||
// this resets the matcher, while keeping the required region
|
||||
matcher.region(relevantStart, relevantEnd);
|
||||
final int punctuationLength = punctuation.length();
|
||||
final var startEnd = new LinkedList<Pair<Integer, Integer>>();
|
||||
int removedCount = 0;
|
||||
while (matcher.find()) {
|
||||
startEnd.add(new Pair<>(matcher.start(), matcher.end()));
|
||||
removedCount += punctuationLength;
|
||||
}
|
||||
// start from the end
|
||||
Collections.reverse(startEnd);
|
||||
for (final var item : startEnd) {
|
||||
deletePunctuation(editable, punctuationLength, item.first, item.second);
|
||||
}
|
||||
int offsetAtEnd = 0;
|
||||
// depending on if the user has selected the markdown chars, we might need to add an offset to the resulting cursor position
|
||||
if (initialString.substring(Math.max(selectionEnd - punctuationLength + 1, 0), Math.min(selectionEnd + 1, initialString.length())).equals(punctuation) ||
|
||||
initialString.substring(selectionEnd, Math.min(selectionEnd + punctuationLength, initialString.length())).equals(punctuation)) {
|
||||
offsetAtEnd = punctuationLength;
|
||||
}
|
||||
return selectionEnd - removedCount * 2 + offsetAtEnd;
|
||||
// ^
|
||||
// start+end, need to double
|
||||
}
|
||||
|
||||
// do nothing when punctuation is contained only once
|
||||
if (Pattern.compile(punctuationRex).matcher(initialString).region(selectionStart, selectionEnd).find()) {
|
||||
return selectionEnd;
|
||||
}
|
||||
|
||||
// nothing returned so far, so it has to be an insertion
|
||||
return insertPunctuation(editable, selectionStart, selectionEnd, punctuation);
|
||||
}
|
||||
|
||||
private static void deletePunctuation(Editable editable, int punctuationLength, int start, int end) {
|
||||
editable.delete(end - punctuationLength, end);
|
||||
editable.delete(start, start + punctuationLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return an {@link Optional<Integer>} of the new cursor position.
|
||||
* The return value is only {@link Optional#isPresent()}, if this is an italic edge case.
|
||||
*/
|
||||
@NonNull
|
||||
private static Optional<Integer> handleItalicEdgeCase(Editable editable, String editableAsString, int selectionStart, int selectionEnd) {
|
||||
// look if selection is bold, this is the only edge case afaik
|
||||
final var searchPattern = Pattern.compile("(^|[^*])" + PATTERN_QUOTE_BOLD_PUNCTUATION + "([^*])*" + PATTERN_QUOTE_BOLD_PUNCTUATION + "([^*]|$)");
|
||||
// look the selection expansion by 1 is intended, so the NOT '*' has a chance to match. we don't want to match ***blah***
|
||||
final var matcher = searchPattern.matcher(editableAsString)
|
||||
.region(Math.max(selectionStart - 1, 0), Math.min(selectionEnd + 1, editableAsString.length()));
|
||||
if (matcher.find()) {
|
||||
return Optional.of(insertPunctuation(editable, selectionStart, selectionEnd, "*"));
|
||||
}
|
||||
// look around (3 chars) (NOT '*' + "**"). User might have selected the text only
|
||||
if (matcher.region(Math.max(selectionStart - 3, 0), Math.min(selectionEnd + 3, editableAsString.length())).find()) {
|
||||
return Optional.of(insertPunctuation(editable, selectionStart, selectionEnd, "*"));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static int insertPunctuation(Editable editable, int firstPosition, int secondPosition, String punctuation) {
|
||||
editable.insert(secondPosition, punctuation);
|
||||
editable.insert(firstPosition, punctuation);
|
||||
return secondPosition + punctuation.length();
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a link into the given {@param editable} from {@param selectionStart} to {@param selectionEnd} and uses the {@param clipboardUrl} if available.
|
||||
*
|
||||
* @return the new cursor position
|
||||
*/
|
||||
public static int insertLink(@NonNull Editable editable, int selectionStart, int selectionEnd, @Nullable String clipboardUrl) {
|
||||
if (selectionStart == selectionEnd) {
|
||||
if (selectionStart > 0 && selectionEnd < editable.length()) {
|
||||
char start = editable.charAt(selectionStart - 1);
|
||||
char end = editable.charAt(selectionEnd);
|
||||
if (start == ' ' || end == ' ') {
|
||||
if (start != ' ') {
|
||||
editable.insert(selectionStart, " ");
|
||||
selectionStart += 1;
|
||||
}
|
||||
if (end != ' ') {
|
||||
editable.insert(selectionEnd, " ");
|
||||
}
|
||||
editable.insert(selectionStart, "[](" + (clipboardUrl == null ? "" : clipboardUrl) + ")");
|
||||
return selectionStart + 1;
|
||||
|
||||
} else {
|
||||
while (start != ' ') {
|
||||
selectionStart--;
|
||||
start = editable.charAt(selectionStart);
|
||||
}
|
||||
selectionStart++;
|
||||
while (end != ' ') {
|
||||
selectionEnd++;
|
||||
end = editable.charAt(selectionEnd);
|
||||
}
|
||||
selectionEnd++;
|
||||
editable.insert(selectionStart, "[");
|
||||
editable.insert(selectionEnd, "](" + (clipboardUrl == null ? "" : clipboardUrl) + ")");
|
||||
if (clipboardUrl != null) {
|
||||
selectionEnd += clipboardUrl.length();
|
||||
}
|
||||
return selectionEnd + 2;
|
||||
}
|
||||
} else {
|
||||
editable.insert(selectionStart, "[](" + (clipboardUrl == null ? "" : clipboardUrl) + ")");
|
||||
return selectionStart + 1;
|
||||
}
|
||||
|
||||
} else {
|
||||
final boolean textToFormatIsLink = TextUtils.indexOf(editable.subSequence(selectionStart, selectionEnd), "http") == 0;
|
||||
if (textToFormatIsLink) {
|
||||
if (clipboardUrl == null) {
|
||||
editable.insert(selectionEnd, ")");
|
||||
editable.insert(selectionStart, "[](");
|
||||
} else {
|
||||
editable.insert(selectionEnd, "](" + clipboardUrl + ")");
|
||||
editable.insert(selectionStart, "[");
|
||||
selectionEnd += clipboardUrl.length();
|
||||
}
|
||||
} else {
|
||||
if (clipboardUrl == null) {
|
||||
editable.insert(selectionEnd, "]()");
|
||||
} else {
|
||||
editable.insert(selectionEnd, "](" + clipboardUrl + ")");
|
||||
selectionEnd += clipboardUrl.length();
|
||||
}
|
||||
editable.insert(selectionStart, "[");
|
||||
}
|
||||
return textToFormatIsLink && clipboardUrl == null
|
||||
? selectionStart + 1
|
||||
: selectionEnd + 3;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static boolean selectionIsInLink(@NonNull CharSequence text, int start, int end) {
|
||||
final var matcher = PATTERN_MARKDOWN_LINK.matcher(text);
|
||||
while (matcher.find()) {
|
||||
if ((start >= matcher.start() && start < matcher.end()) || (end > matcher.start() && end <= matcher.end())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void searchAndColor(@NonNull Spannable editable, @Nullable CharSequence searchText, @Nullable Integer current, @ColorInt int mainColor, @ColorInt int highlightColor, boolean darkTheme) {
|
||||
if (searchText != null) {
|
||||
final var m = Pattern
|
||||
.compile(searchText.toString(), Pattern.CASE_INSENSITIVE | Pattern.LITERAL)
|
||||
.matcher(editable);
|
||||
|
||||
int i = 1;
|
||||
while (m.find()) {
|
||||
int start = m.start();
|
||||
int end = m.end();
|
||||
editable.setSpan(new SearchSpan(mainColor, highlightColor, (current != null && i == current), darkTheme), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all spans of {@param spanType} from {@param spannable}.
|
||||
*/
|
||||
public static <T> void removeSpans(@NonNull Spannable spannable, @SuppressWarnings("SameParameterValue") Class<T> spanType) {
|
||||
for (final var span : spannable.getSpans(0, spannable.length(), spanType)) {
|
||||
spannable.removeSpan(span);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return When the content of the {@param textView} is already of type {@link Spannable}, it will cast and return it directly.
|
||||
* Otherwise it will create a new {@link SpannableString} from the content, set this as new content of the {@param textView} and return it.
|
||||
*/
|
||||
public static Spannable getContentAsSpannable(@NonNull TextView textView) {
|
||||
final var content = textView.getText();
|
||||
if (content.getClass() == SpannableString.class || content instanceof Spannable) {
|
||||
return (Spannable) content;
|
||||
} else {
|
||||
Log.w(TAG, "Expected " + TextView.class.getSimpleName() + " content to be of type " + Spannable.class.getSimpleName() + ", but was of type " + content.getClass() + ". Search highlighting will be not performant.");
|
||||
final var spannableContent = new SpannableString(content);
|
||||
textView.setText(spannableContent, TextView.BufferType.SPANNABLE);
|
||||
return spannableContent;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getMarkdownLink(@NonNull String text, @NonNull String url) {
|
||||
return "[" + text + "](" + url + ")";
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips all Markdown from {@param s}
|
||||
*
|
||||
* @param s Markdown string
|
||||
* @return Plain text string
|
||||
*/
|
||||
@NonNull
|
||||
public static String removeMarkdown(@Nullable String s) {
|
||||
if (TextUtils.isEmpty(s)) {
|
||||
return "";
|
||||
}
|
||||
assert s != null;
|
||||
final String html = RENDERER.render(PARSER.parse(replaceCheckboxesWithEmojis(s)));
|
||||
final Spanned spanned = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT);
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
||||
return spanned.toString().trim().replaceAll("\n\n", "\n");
|
||||
}
|
||||
return spanned.toString().trim();
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
package it.niedermann.android.markdown;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import it.niedermann.android.markdown.markwon.MarkwonMarkdownViewer;
|
||||
|
||||
public class MarkdownViewerImpl extends MarkwonMarkdownViewer {
|
||||
|
||||
public MarkdownViewerImpl(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public MarkdownViewerImpl(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public MarkdownViewerImpl(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
package it.niedermann.android.markdown;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.ImageSpan;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RestrictTo;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.request.RequestOptions;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.nextcloud.android.sso.model.SingleSignOnAccount;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
||||
public class MentionUtil {
|
||||
|
||||
private MentionUtil() {
|
||||
// Util class
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces all mentions in the textView with an avatar and the display name
|
||||
*
|
||||
* @param account {@link SingleSignOnAccount} where the users of those mentions belong to
|
||||
* @param mentions {@link Map} of all mentions that should be substituted, the key is the user id and the value the display name
|
||||
* @param target target {@link TextView}
|
||||
*/
|
||||
public static void setupMentions(@NonNull SingleSignOnAccount account, @NonNull Map<String, String> mentions, @NonNull TextView target) {
|
||||
final var context = target.getContext();
|
||||
|
||||
// Step 1
|
||||
// Add avatar icons and display names
|
||||
final var messageBuilder = replaceAtMentionsWithImagePlaceholderAndDisplayName(context, mentions, target.getText());
|
||||
|
||||
// Step 2
|
||||
// Replace avatar icons with real avatars
|
||||
final var list = messageBuilder.getSpans(0, messageBuilder.length(), MentionSpan.class);
|
||||
for (final var span : list) {
|
||||
final int spanStart = messageBuilder.getSpanStart(span);
|
||||
final int spanEnd = messageBuilder.getSpanEnd(span);
|
||||
Glide.with(context)
|
||||
.asBitmap()
|
||||
.placeholder(R.drawable.ic_person_grey600_24dp)
|
||||
.load(account.url + "/index.php/avatar/" + messageBuilder.subSequence(spanStart + 1, spanEnd).toString() + "/" + span.getDrawable().getIntrinsicHeight())
|
||||
.apply(RequestOptions.circleCropTransform())
|
||||
.into(new CustomTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
||||
messageBuilder.removeSpan(span);
|
||||
messageBuilder.setSpan(new MentionSpan(context, resource), spanStart, spanEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCleared(@Nullable Drawable placeholder) {
|
||||
// silence is gold
|
||||
}
|
||||
});
|
||||
}
|
||||
target.setText(messageBuilder);
|
||||
}
|
||||
|
||||
private static SpannableStringBuilder replaceAtMentionsWithImagePlaceholderAndDisplayName(@NonNull Context context, @NonNull Map<String, String> mentions, @NonNull CharSequence text) {
|
||||
final var messageBuilder = new SpannableStringBuilder(text);
|
||||
for (String userId : mentions.keySet()) {
|
||||
final String mentionId = "@" + userId;
|
||||
final String mentionDisplayName = " " + mentions.get(userId);
|
||||
int index = messageBuilder.toString().lastIndexOf(mentionId);
|
||||
while (index >= 0) {
|
||||
messageBuilder.setSpan(new MentionSpan(context, R.drawable.ic_person_grey600_24dp), index, index + mentionId.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
messageBuilder.insert(index + mentionId.length(), mentionDisplayName);
|
||||
index = messageBuilder.toString().substring(0, index).lastIndexOf(mentionId);
|
||||
}
|
||||
}
|
||||
return messageBuilder;
|
||||
}
|
||||
|
||||
private static class MentionSpan extends ImageSpan {
|
||||
private MentionSpan(@NonNull Context context, int resourceId) {
|
||||
super(context, resourceId);
|
||||
}
|
||||
|
||||
private MentionSpan(@NonNull Context context, @NonNull Bitmap bitmap) {
|
||||
super(context, bitmap);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,143 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.AppCompatEditText;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.editor.MarkwonEditor;
|
||||
import io.noties.markwon.editor.handler.EmphasisEditHandler;
|
||||
import io.noties.markwon.editor.handler.StrongEmphasisEditHandler;
|
||||
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
|
||||
import io.noties.markwon.image.ImagesPlugin;
|
||||
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
|
||||
import io.noties.markwon.simple.ext.SimpleExtPlugin;
|
||||
import it.niedermann.android.markdown.MarkdownEditor;
|
||||
import it.niedermann.android.markdown.markwon.format.ContextBasedFormattingCallback;
|
||||
import it.niedermann.android.markdown.markwon.format.ContextBasedRangeFormattingCallback;
|
||||
import it.niedermann.android.markdown.markwon.handler.BlockQuoteEditHandler;
|
||||
import it.niedermann.android.markdown.markwon.handler.CodeBlockEditHandler;
|
||||
import it.niedermann.android.markdown.markwon.handler.CodeEditHandler;
|
||||
import it.niedermann.android.markdown.markwon.handler.HeadingEditHandler;
|
||||
import it.niedermann.android.markdown.markwon.handler.StrikethroughEditHandler;
|
||||
import it.niedermann.android.markdown.markwon.plugins.SearchHighlightPlugin;
|
||||
import it.niedermann.android.markdown.markwon.plugins.ThemePlugin;
|
||||
import it.niedermann.android.markdown.markwon.textwatcher.CombinedTextWatcher;
|
||||
import it.niedermann.android.markdown.markwon.textwatcher.SearchHighlightTextWatcher;
|
||||
|
||||
public class MarkwonMarkdownEditor extends AppCompatEditText implements MarkdownEditor {
|
||||
|
||||
private static final String TAG = MarkwonMarkdownEditor.class.getSimpleName();
|
||||
|
||||
@Nullable
|
||||
private Consumer<CharSequence> listener;
|
||||
private final MutableLiveData<CharSequence> unrenderedText$ = new MutableLiveData<>();
|
||||
private final CombinedTextWatcher combinedWatcher;
|
||||
|
||||
public MarkwonMarkdownEditor(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public MarkwonMarkdownEditor(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs, android.R.attr.editTextStyle);
|
||||
}
|
||||
|
||||
public MarkwonMarkdownEditor(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
final var markwon = createMarkwonBuilder(context).build();
|
||||
final var editor = createMarkwonEditorBuilder(markwon).build();
|
||||
|
||||
combinedWatcher = new CombinedTextWatcher(editor, this);
|
||||
addTextChangedListener(combinedWatcher);
|
||||
setCustomSelectionActionModeCallback(new ContextBasedRangeFormattingCallback(this));
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
setCustomInsertionActionModeCallback(new ContextBasedFormattingCallback(this));
|
||||
}
|
||||
}
|
||||
|
||||
private static Markwon.Builder createMarkwonBuilder(@NonNull Context context) {
|
||||
return Markwon.builder(context)
|
||||
.usePlugin(ThemePlugin.create(context))
|
||||
.usePlugin(StrikethroughPlugin.create())
|
||||
.usePlugin(SimpleExtPlugin.create())
|
||||
.usePlugin(ImagesPlugin.create())
|
||||
.usePlugin(MarkwonInlineParserPlugin.create())
|
||||
.usePlugin(SearchHighlightPlugin.create(context));
|
||||
}
|
||||
|
||||
private static MarkwonEditor.Builder createMarkwonEditorBuilder(@NonNull Markwon markwon) {
|
||||
return MarkwonEditor.builder(markwon)
|
||||
.useEditHandler(new EmphasisEditHandler())
|
||||
.useEditHandler(new StrongEmphasisEditHandler())
|
||||
.useEditHandler(new StrikethroughEditHandler())
|
||||
.useEditHandler(new CodeEditHandler())
|
||||
.useEditHandler(new CodeBlockEditHandler())
|
||||
.useEditHandler(new BlockQuoteEditHandler())
|
||||
.useEditHandler(new HeadingEditHandler());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSearchColor(@ColorInt int color) {
|
||||
final var searchHighlightTextWatcher = combinedWatcher.get(SearchHighlightTextWatcher.class);
|
||||
if (searchHighlightTextWatcher == null) {
|
||||
Log.w(TAG, SearchHighlightTextWatcher.class.getSimpleName() + " is not a registered " + TextWatcher.class.getSimpleName());
|
||||
} else {
|
||||
searchHighlightTextWatcher.setSearchColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSearchText(@Nullable CharSequence searchText, @Nullable Integer current) {
|
||||
final var searchHighlightTextWatcher = combinedWatcher.get(SearchHighlightTextWatcher.class);
|
||||
if (searchHighlightTextWatcher == null) {
|
||||
Log.w(TAG, SearchHighlightTextWatcher.class.getSimpleName() + " is not a registered " + TextWatcher.class.getSimpleName());
|
||||
} else {
|
||||
searchHighlightTextWatcher.setSearchText(searchText, current);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMarkdownString(CharSequence text) {
|
||||
setText(text);
|
||||
setMarkdownStringModel(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMarkdownString(CharSequence text, Runnable afterRender) {
|
||||
throw new UnsupportedOperationException("This is not available in " + MarkwonMarkdownEditor.class.getSimpleName() + " because the text is getting rendered all the time.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current model which matches the rendered state of the editor *without* triggering
|
||||
* anything of the native {@link EditText}
|
||||
*/
|
||||
public void setMarkdownStringModel(CharSequence text) {
|
||||
unrenderedText$.setValue(text == null ? "" : text.toString());
|
||||
if (listener != null) {
|
||||
listener.accept(text);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public LiveData<CharSequence> getMarkdownString() {
|
||||
return unrenderedText$;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMarkdownStringChangedListener(@Nullable Consumer<CharSequence> listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class MarkwonMarkdownUtil {
|
||||
|
||||
private MarkwonMarkdownUtil() {
|
||||
// Util class
|
||||
}
|
||||
|
||||
public static boolean isDarkThemeActive(@NonNull Context context) {
|
||||
final int uiMode = context.getResources().getConfiguration().uiMode;
|
||||
return (uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
|
||||
}
|
||||
}
|
|
@ -1,192 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonPlugin;
|
||||
import io.noties.markwon.SoftBreakAddsNewLinePlugin;
|
||||
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
|
||||
import io.noties.markwon.ext.tables.TableAwareMovementMethod;
|
||||
import io.noties.markwon.ext.tables.TablePlugin;
|
||||
import io.noties.markwon.ext.tasklist.TaskListPlugin;
|
||||
import io.noties.markwon.image.glide.GlideImagesPlugin;
|
||||
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
|
||||
import io.noties.markwon.linkify.LinkifyPlugin;
|
||||
import io.noties.markwon.movement.MovementMethodPlugin;
|
||||
import io.noties.markwon.simple.ext.SimpleExtPlugin;
|
||||
import io.noties.markwon.syntax.Prism4jTheme;
|
||||
import io.noties.markwon.syntax.Prism4jThemeDarkula;
|
||||
import io.noties.markwon.syntax.Prism4jThemeDefault;
|
||||
import io.noties.markwon.syntax.SyntaxHighlightPlugin;
|
||||
import io.noties.prism4j.Prism4j;
|
||||
import io.noties.prism4j.annotations.PrismBundle;
|
||||
import it.niedermann.android.markdown.MarkdownEditor;
|
||||
import it.niedermann.android.markdown.MarkdownUtil;
|
||||
import it.niedermann.android.markdown.markwon.plugins.CustomGlideStore;
|
||||
import it.niedermann.android.markdown.markwon.plugins.LinkClickInterceptorPlugin;
|
||||
import it.niedermann.android.markdown.markwon.plugins.NextcloudMentionsPlugin;
|
||||
import it.niedermann.android.markdown.markwon.plugins.SearchHighlightPlugin;
|
||||
import it.niedermann.android.markdown.markwon.plugins.ThemePlugin;
|
||||
import it.niedermann.android.markdown.markwon.plugins.ToggleableTaskListPlugin;
|
||||
|
||||
import static androidx.lifecycle.Transformations.distinctUntilChanged;
|
||||
|
||||
@PrismBundle(includeAll = true, grammarLocatorClassName = ".MarkwonGrammarLocator")
|
||||
public class MarkwonMarkdownViewer extends AppCompatTextView implements MarkdownEditor {
|
||||
|
||||
private static final String TAG = MarkwonMarkdownViewer.class.getSimpleName();
|
||||
|
||||
private static final Prism4j prism4j = new Prism4j(new MarkwonGrammarLocator());
|
||||
|
||||
private Markwon markwon;
|
||||
@Nullable
|
||||
private Consumer<CharSequence> listener = null;
|
||||
private final MutableLiveData<CharSequence> unrenderedText$ = new MutableLiveData<>();
|
||||
|
||||
private final ExecutorService renderService;
|
||||
|
||||
public MarkwonMarkdownViewer(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public MarkwonMarkdownViewer(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs, android.R.attr.textViewStyle);
|
||||
}
|
||||
|
||||
public MarkwonMarkdownViewer(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
this.markwon = createMarkwonBuilder(context).build();
|
||||
this.renderService = Executors.newSingleThreadExecutor();
|
||||
}
|
||||
|
||||
private Markwon.Builder createMarkwonBuilder(@NonNull Context context) {
|
||||
final Prism4jTheme prism4jTheme = MarkwonMarkdownUtil.isDarkThemeActive(context)
|
||||
? Prism4jThemeDarkula.create()
|
||||
: Prism4jThemeDefault.create();
|
||||
return Markwon.builder(context)
|
||||
.usePlugin(ThemePlugin.create(context))
|
||||
.usePlugin(StrikethroughPlugin.create())
|
||||
.usePlugin(SimpleExtPlugin.create())
|
||||
.usePlugin(MarkwonInlineParserPlugin.create())
|
||||
.usePlugin(SearchHighlightPlugin.create(context))
|
||||
.usePlugin(TablePlugin.create(context))
|
||||
.usePlugin(TaskListPlugin.create(context))
|
||||
.usePlugin(LinkifyPlugin.create(true))
|
||||
.usePlugin(MovementMethodPlugin.create(TableAwareMovementMethod.create()))
|
||||
.usePlugin(LinkClickInterceptorPlugin.create())
|
||||
.usePlugin(GlideImagesPlugin.create(new CustomGlideStore(context)))
|
||||
.usePlugin(SoftBreakAddsNewLinePlugin.create())
|
||||
.usePlugin(SyntaxHighlightPlugin.create(prism4j, prism4jTheme))
|
||||
.usePlugin(new ToggleableTaskListPlugin((toggledCheckboxPosition, newCheckedState) -> {
|
||||
final var oldUnrenderedText = unrenderedText$.getValue();
|
||||
if (oldUnrenderedText == null) {
|
||||
throw new IllegalStateException("Checkbox #" + toggledCheckboxPosition + ", but unrenderedText$ value is null.");
|
||||
}
|
||||
final var newUnrenderedText = MarkdownUtil.setCheckboxStatus(oldUnrenderedText.toString(), toggledCheckboxPosition, newCheckedState);
|
||||
this.setMarkdownString(newUnrenderedText);
|
||||
}));
|
||||
}
|
||||
|
||||
public Markwon.Builder createMarkwonBuilder(@NonNull Context context, @NonNull Map<String, String> mentions) {
|
||||
return createMarkwonBuilder(context)
|
||||
.usePlugin(NextcloudMentionsPlugin.create(context, mentions));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerOnLinkClickCallback(@NonNull Function<String, Boolean> callback) {
|
||||
final var plugin = this.markwon.getPlugin(LinkClickInterceptorPlugin.class);
|
||||
if (plugin == null) {
|
||||
Log.w(TAG, "Tried to register callback, but " + LinkClickInterceptorPlugin.class.getSimpleName() + " is not a registered " + MarkwonPlugin.class.getSimpleName() + ".");
|
||||
} else {
|
||||
plugin.registerOnLinkClickCallback(callback);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabled(boolean enabled) {
|
||||
final var plugin = this.markwon.getPlugin(ToggleableTaskListPlugin.class);
|
||||
if (plugin == null) {
|
||||
Log.w(TAG, "Tried to set enabled state for " + ToggleableTaskListPlugin.class.getSimpleName() + ", but " + ToggleableTaskListPlugin.class.getSimpleName() + " is not a registered " + MarkwonPlugin.class.getSimpleName() + ".");
|
||||
} else {
|
||||
plugin.setEnabled(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMarkdownString(CharSequence text) {
|
||||
setMarkdownString(text, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMarkdownString(CharSequence text, Runnable afterRender) {
|
||||
final var previousText = this.unrenderedText$.getValue();
|
||||
this.unrenderedText$.setValue(text);
|
||||
if (listener != null) {
|
||||
listener.accept(text);
|
||||
}
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
setText(text);
|
||||
} else {
|
||||
if (!text.equals(previousText)) {
|
||||
this.renderService.execute(() -> post(() -> {
|
||||
this.markwon.setMarkdown(this, text.toString());
|
||||
if (afterRender != null) {
|
||||
afterRender.run();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSearchColor(@ColorInt int color) {
|
||||
final var searchHighlightPlugin = this.markwon.getPlugin(SearchHighlightPlugin.class);
|
||||
if (searchHighlightPlugin == null) {
|
||||
Log.w(TAG, SearchHighlightPlugin.class.getSimpleName() + " is not a registered " + MarkwonPlugin.class.getSimpleName());
|
||||
} else {
|
||||
searchHighlightPlugin.setSearchColor(color, this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSearchText(@Nullable CharSequence searchText, @Nullable Integer current) {
|
||||
final var searchHighlightPlugin = this.markwon.getPlugin(SearchHighlightPlugin.class);
|
||||
if (searchHighlightPlugin == null) {
|
||||
Log.w(TAG, SearchHighlightPlugin.class.getSimpleName() + " is not a registered " + MarkwonPlugin.class.getSimpleName());
|
||||
} else {
|
||||
searchHighlightPlugin.setSearchText(searchText, current, this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMarkdownStringAndHighlightMentions(CharSequence text, @NonNull Map<String, String> mentions) {
|
||||
this.markwon = createMarkwonBuilder(getContext(), mentions).build();
|
||||
setMarkdownString(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LiveData<CharSequence> getMarkdownString() {
|
||||
return distinctUntilChanged(this.unrenderedText$);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMarkdownStringChangedListener(@Nullable Consumer<CharSequence> listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.format;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.util.Log;
|
||||
import android.view.ActionMode;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import it.niedermann.android.markdown.R;
|
||||
import it.niedermann.android.markdown.markwon.MarkwonMarkdownEditor;
|
||||
import it.niedermann.android.markdown.MarkdownUtil;
|
||||
import it.niedermann.android.markdown.model.EListType;
|
||||
import it.niedermann.android.util.ClipboardUtil;
|
||||
|
||||
import static it.niedermann.android.markdown.MarkdownUtil.getEndOfLine;
|
||||
import static it.niedermann.android.markdown.MarkdownUtil.getStartOfLine;
|
||||
import static it.niedermann.android.markdown.MarkdownUtil.lineStartsWithCheckbox;
|
||||
|
||||
public class ContextBasedFormattingCallback implements ActionMode.Callback {
|
||||
|
||||
private static final String TAG = ContextBasedFormattingCallback.class.getSimpleName();
|
||||
|
||||
private final MarkwonMarkdownEditor editText;
|
||||
|
||||
public ContextBasedFormattingCallback(MarkwonMarkdownEditor editText) {
|
||||
this.editText = editText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
mode.getMenuInflater().inflate(R.menu.context_based_formatting, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
final var text = editText.getText();
|
||||
if (text != null) {
|
||||
final int cursorPosition = editText.getSelectionStart();
|
||||
if (cursorPosition >= 0 && cursorPosition <= text.length()) {
|
||||
final int startOfLine = getStartOfLine(text, cursorPosition);
|
||||
final int endOfLine = getEndOfLine(text, cursorPosition);
|
||||
final String line = text.subSequence(startOfLine, endOfLine).toString();
|
||||
if (lineStartsWithCheckbox(line)) {
|
||||
menu.findItem(R.id.checkbox).setVisible(false);
|
||||
Log.i(TAG, "Hide checkbox menu item because line starts already with checkbox");
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "SelectionStart is " + cursorPosition + ". Expected to be between 0 and " + text.length());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
final var editable = editText.getText();
|
||||
if (editable != null) {
|
||||
final int itemId = item.getItemId();
|
||||
final int cursorPosition = editText.getSelectionStart();
|
||||
|
||||
if (itemId == R.id.checkbox) {
|
||||
editable.insert(getStartOfLine(editable, cursorPosition), EListType.DASH.checkboxUncheckedWithTrailingSpace);
|
||||
editText.setMarkdownStringModel(editable);
|
||||
editText.setSelection(cursorPosition + EListType.DASH.checkboxUncheckedWithTrailingSpace.length());
|
||||
return true;
|
||||
} else if (itemId == R.id.link) {
|
||||
final int newSelection = MarkdownUtil.insertLink(editable, cursorPosition, cursorPosition, ClipboardUtil.INSTANCE.getClipboardURLorNull(editText.getContext()));
|
||||
editText.setMarkdownStringModel(editable);
|
||||
editText.setSelection(newSelection);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Editable is null");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
// Nothing to do here...
|
||||
}
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.format;
|
||||
|
||||
import android.graphics.Typeface;
|
||||
import android.text.Editable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.util.Log;
|
||||
import android.util.SparseIntArray;
|
||||
import android.view.ActionMode;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import it.niedermann.android.markdown.R;
|
||||
import it.niedermann.android.markdown.markwon.MarkwonMarkdownEditor;
|
||||
import it.niedermann.android.markdown.MarkdownUtil;
|
||||
import it.niedermann.android.util.ClipboardUtil;
|
||||
|
||||
public class ContextBasedRangeFormattingCallback implements ActionMode.Callback {
|
||||
|
||||
private static final String TAG = ContextBasedRangeFormattingCallback.class.getSimpleName();
|
||||
|
||||
private final MarkwonMarkdownEditor editText;
|
||||
|
||||
public ContextBasedRangeFormattingCallback(MarkwonMarkdownEditor editText) {
|
||||
this.editText = editText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
mode.getMenuInflater().inflate(R.menu.context_based_range_formatting, menu);
|
||||
|
||||
final var styleFormatMap = new SparseIntArray();
|
||||
styleFormatMap.append(R.id.bold, Typeface.BOLD);
|
||||
styleFormatMap.append(R.id.italic, Typeface.ITALIC);
|
||||
|
||||
MenuItem item;
|
||||
CharSequence title;
|
||||
SpannableString spannableString;
|
||||
|
||||
for (int i = 0; i < styleFormatMap.size(); i++) {
|
||||
item = menu.findItem(styleFormatMap.keyAt(i));
|
||||
title = item.getTitle();
|
||||
spannableString = new SpannableString(title);
|
||||
spannableString.setSpan(new StyleSpan(styleFormatMap.valueAt(i)), 0, title.length(), 0);
|
||||
item.setTitle(spannableString);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
final var text = editText.getText();
|
||||
if (text != null) {
|
||||
final int selectionStart = editText.getSelectionStart();
|
||||
final int selectionEnd = editText.getSelectionEnd();
|
||||
if (selectionStart >= 0 && selectionStart <= text.length()) {
|
||||
if (MarkdownUtil.selectionIsInLink(text, selectionStart, selectionEnd)) {
|
||||
menu.findItem(R.id.link).setVisible(false);
|
||||
Log.i(TAG, "Hide link menu item because the selection is already within a link.");
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "SelectionStart is " + selectionStart + ". Expected to be between 0 and " + text.length());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
final var editable = editText.getText();
|
||||
if (editable != null) {
|
||||
final int itemId = item.getItemId();
|
||||
final int start = editText.getSelectionStart();
|
||||
final int end = editText.getSelectionEnd();
|
||||
|
||||
if (itemId == R.id.bold) {
|
||||
final int newSelection = MarkdownUtil.togglePunctuation(editable, start, end, "**");
|
||||
editText.setMarkdownStringModel(editable);
|
||||
editText.setSelection(newSelection);
|
||||
return true;
|
||||
} else if (itemId == R.id.italic) {
|
||||
final int newSelection = MarkdownUtil.togglePunctuation(editable, start, end, "*");
|
||||
editText.setMarkdownStringModel(editable);
|
||||
editText.setSelection(newSelection);
|
||||
return true;
|
||||
} else if (itemId == R.id.link) {
|
||||
final int newSelection = MarkdownUtil.insertLink(editable, start, end, ClipboardUtil.INSTANCE.getClipboardURLorNull(editText.getContext()));
|
||||
editText.setMarkdownStringModel(editable);
|
||||
editText.setSelection(newSelection);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "Editable is null");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
// Nothing to do here...
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.glide;
|
||||
|
||||
import androidx.annotation.Px;
|
||||
|
||||
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy;
|
||||
|
||||
/**
|
||||
* @see <a href="https://github.com/noties/Markwon/issues/329#issuecomment-855220315">Source</a>
|
||||
*/
|
||||
public class DownsampleWithMaxWidth extends DownsampleStrategy {
|
||||
|
||||
@Px
|
||||
private final int maxWidth;
|
||||
|
||||
public DownsampleWithMaxWidth(@Px int maxWidth) {
|
||||
this.maxWidth = maxWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getScaleFactor(int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) {
|
||||
// do not scale down if fits requested dimension
|
||||
if (sourceWidth < maxWidth) {
|
||||
return 1F;
|
||||
}
|
||||
return (float) maxWidth / sourceWidth;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SampleSizeRounding getSampleSizeRounding(int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) {
|
||||
// go figure
|
||||
return SampleSizeRounding.MEMORY;
|
||||
}
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.handler;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.core.MarkwonTheme;
|
||||
import io.noties.markwon.core.spans.BlockQuoteSpan;
|
||||
import io.noties.markwon.editor.EditHandler;
|
||||
import io.noties.markwon.editor.PersistedSpans;
|
||||
|
||||
public class BlockQuoteEditHandler implements EditHandler<BlockQuoteSpan> {
|
||||
|
||||
private MarkwonTheme theme;
|
||||
|
||||
@Override
|
||||
public void init(@NonNull Markwon markwon) {
|
||||
this.theme = markwon.configuration().theme();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
|
||||
builder.persistSpan(BlockQuoteSpan.class, () -> new BlockQuoteSpan(theme));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMarkdownSpan(
|
||||
@NonNull PersistedSpans persistedSpans,
|
||||
@NonNull Editable editable,
|
||||
@NonNull String input,
|
||||
@NonNull BlockQuoteSpan span,
|
||||
int spanStart,
|
||||
int spanTextLength) {
|
||||
// todo: here we should actually find a proper ending of a block quote...
|
||||
editable.setSpan(
|
||||
persistedSpans.get(BlockQuoteSpan.class),
|
||||
spanStart,
|
||||
spanStart + spanTextLength,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<BlockQuoteSpan> markdownSpanType() {
|
||||
return BlockQuoteSpan.class;
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.handler;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.core.MarkwonTheme;
|
||||
import io.noties.markwon.core.spans.CodeBlockSpan;
|
||||
import io.noties.markwon.editor.EditHandler;
|
||||
import io.noties.markwon.editor.MarkwonEditorUtils;
|
||||
import io.noties.markwon.editor.PersistedSpans;
|
||||
|
||||
public class CodeBlockEditHandler implements EditHandler<CodeBlockSpan> {
|
||||
private MarkwonTheme theme;
|
||||
|
||||
@Override
|
||||
public void init(@NonNull Markwon markwon) {
|
||||
theme = markwon.configuration().theme();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
|
||||
builder.persistSpan(CodeBlockSpan.class, () -> new CodeBlockSpan(theme));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMarkdownSpan(@NonNull PersistedSpans persistedSpans, @NonNull Editable editable, @NonNull String input, @NonNull CodeBlockSpan span, int spanStart, int spanTextLength) {
|
||||
final var delimited = MarkwonEditorUtils.findDelimited(input, spanStart, "```");
|
||||
if (delimited != null) {
|
||||
editable.setSpan(
|
||||
persistedSpans.get(markdownSpanType()),
|
||||
delimited.start(),
|
||||
delimited.end(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<CodeBlockSpan> markdownSpanType() {
|
||||
return CodeBlockSpan.class;
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.handler;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.core.MarkwonTheme;
|
||||
import io.noties.markwon.core.spans.CodeSpan;
|
||||
import io.noties.markwon.editor.EditHandler;
|
||||
import io.noties.markwon.editor.MarkwonEditorUtils;
|
||||
import io.noties.markwon.editor.PersistedSpans;
|
||||
|
||||
public class CodeEditHandler implements EditHandler<CodeSpan> {
|
||||
|
||||
private MarkwonTheme theme;
|
||||
|
||||
@Override
|
||||
public void init(@NonNull Markwon markwon) {
|
||||
this.theme = markwon.configuration().theme();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
|
||||
builder.persistSpan(CodeSpan.class, () -> new CodeSpan(theme));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMarkdownSpan(
|
||||
@NonNull PersistedSpans persistedSpans,
|
||||
@NonNull Editable editable,
|
||||
@NonNull String input,
|
||||
@NonNull CodeSpan span,
|
||||
int spanStart,
|
||||
int spanTextLength) {
|
||||
final var match = MarkwonEditorUtils.findDelimited(input, spanStart, "`");
|
||||
if (match != null) {
|
||||
editable.setSpan(
|
||||
persistedSpans.get(CodeSpan.class),
|
||||
match.start(),
|
||||
match.end(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<CodeSpan> markdownSpanType() {
|
||||
return CodeSpan.class;
|
||||
}
|
||||
}
|
|
@ -1,142 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.handler;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.core.MarkwonTheme;
|
||||
import io.noties.markwon.core.spans.HeadingSpan;
|
||||
import io.noties.markwon.editor.EditHandler;
|
||||
import io.noties.markwon.editor.PersistedSpans;
|
||||
|
||||
public class HeadingEditHandler implements EditHandler<HeadingSpan> {
|
||||
|
||||
private MarkwonTheme theme;
|
||||
|
||||
@Override
|
||||
public void init(@NonNull Markwon markwon) {
|
||||
this.theme = markwon.configuration().theme();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
|
||||
builder.persistSpan(Heading1Span.class, () -> new Heading1Span(theme));
|
||||
builder.persistSpan(Heading2Span.class, () -> new Heading2Span(theme));
|
||||
builder.persistSpan(Heading3Span.class, () -> new Heading3Span(theme));
|
||||
builder.persistSpan(Heading4Span.class, () -> new Heading4Span(theme));
|
||||
builder.persistSpan(Heading5Span.class, () -> new Heading5Span(theme));
|
||||
builder.persistSpan(Heading6Span.class, () -> new Heading6Span(theme));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMarkdownSpan(
|
||||
@NonNull PersistedSpans persistedSpans,
|
||||
@NonNull Editable editable,
|
||||
@NonNull String input,
|
||||
@NonNull HeadingSpan span,
|
||||
int spanStart,
|
||||
int spanTextLength) {
|
||||
final HeadingSpan newSpan;
|
||||
|
||||
switch (span.getLevel()) {
|
||||
case 1:
|
||||
newSpan = persistedSpans.get(Heading1Span.class);
|
||||
break;
|
||||
case 2:
|
||||
newSpan = persistedSpans.get(Heading2Span.class);
|
||||
break;
|
||||
case 3:
|
||||
newSpan = persistedSpans.get(Heading3Span.class);
|
||||
break;
|
||||
case 4:
|
||||
newSpan = persistedSpans.get(Heading4Span.class);
|
||||
break;
|
||||
case 5:
|
||||
newSpan = persistedSpans.get(Heading5Span.class);
|
||||
break;
|
||||
case 6:
|
||||
newSpan = persistedSpans.get(Heading6Span.class);
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
|
||||
}
|
||||
final int newStart = getNewSpanStart(input, spanStart);
|
||||
final int newEnd = findEnd(input, newStart, newSpan.getLevel());
|
||||
|
||||
editable.setSpan(
|
||||
newSpan,
|
||||
newStart,
|
||||
newEnd,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
|
||||
private int findEnd(String input, int searchFrom, int spanLevel) {
|
||||
int end = searchFrom + spanLevel;
|
||||
final int strLength = input.length();
|
||||
while (end < strLength - 1) {
|
||||
end++;
|
||||
if (input.charAt(end) == '\n') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return end + 1;
|
||||
}
|
||||
|
||||
private int getNewSpanStart(String input, int spanStart) {
|
||||
int start = spanStart;
|
||||
|
||||
while (start >= 0 && input.charAt(start) != '\n') {
|
||||
start--;
|
||||
}
|
||||
start += 1;
|
||||
|
||||
return start;
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<HeadingSpan> markdownSpanType() {
|
||||
return HeadingSpan.class;
|
||||
}
|
||||
|
||||
private static class Heading1Span extends HeadingSpan {
|
||||
public Heading1Span(@NonNull MarkwonTheme theme) {
|
||||
super(theme, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Heading2Span extends HeadingSpan {
|
||||
public Heading2Span(@NonNull MarkwonTheme theme) {
|
||||
super(theme, 2);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Heading3Span extends HeadingSpan {
|
||||
public Heading3Span(@NonNull MarkwonTheme theme) {
|
||||
super(theme, 3);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Heading4Span extends HeadingSpan {
|
||||
public Heading4Span(@NonNull MarkwonTheme theme) {
|
||||
super(theme, 4);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Heading5Span extends HeadingSpan {
|
||||
public Heading5Span(@NonNull MarkwonTheme theme) {
|
||||
super(theme, 5);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Heading6Span extends HeadingSpan {
|
||||
public Heading6Span(@NonNull MarkwonTheme theme) {
|
||||
super(theme, 6);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.handler;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.noties.markwon.core.spans.LinkSpan;
|
||||
import io.noties.markwon.editor.AbstractEditHandler;
|
||||
import io.noties.markwon.editor.PersistedSpans;
|
||||
|
||||
public class LinkEditHandler extends AbstractEditHandler<LinkSpan> {
|
||||
|
||||
public interface OnClick {
|
||||
void onClick(@NonNull View widget, @NonNull String link);
|
||||
}
|
||||
|
||||
private final OnClick onClick;
|
||||
|
||||
public LinkEditHandler(@NonNull OnClick onClick) {
|
||||
this.onClick = onClick;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
|
||||
builder.persistSpan(EditLinkSpan.class, () -> new EditLinkSpan(onClick));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMarkdownSpan(
|
||||
@NonNull PersistedSpans persistedSpans,
|
||||
@NonNull Editable editable,
|
||||
@NonNull String input,
|
||||
@NonNull LinkSpan span,
|
||||
int spanStart,
|
||||
int spanTextLength) {
|
||||
|
||||
final var editLinkSpan = persistedSpans.get(EditLinkSpan.class);
|
||||
editLinkSpan.link = span.getLink();
|
||||
|
||||
final int s;
|
||||
final int e;
|
||||
|
||||
// markdown link vs. autolink
|
||||
if ('[' == input.charAt(spanStart)) {
|
||||
s = spanStart + 1;
|
||||
e = spanStart + 1 + spanTextLength;
|
||||
} else {
|
||||
s = spanStart;
|
||||
e = spanStart + spanTextLength;
|
||||
}
|
||||
|
||||
editable.setSpan(
|
||||
editLinkSpan,
|
||||
s,
|
||||
e,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<LinkSpan> markdownSpanType() {
|
||||
return LinkSpan.class;
|
||||
}
|
||||
|
||||
static class EditLinkSpan extends ClickableSpan {
|
||||
|
||||
private final OnClick onClick;
|
||||
|
||||
String link;
|
||||
|
||||
EditLinkSpan(@NonNull OnClick onClick) {
|
||||
this.onClick = onClick;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
if (link != null) {
|
||||
onClick.onClick(widget, link);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.handler;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.StrikethroughSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.noties.markwon.editor.AbstractEditHandler;
|
||||
import io.noties.markwon.editor.MarkwonEditorUtils;
|
||||
import io.noties.markwon.editor.PersistedSpans;
|
||||
|
||||
public class StrikethroughEditHandler extends AbstractEditHandler<StrikethroughSpan> {
|
||||
|
||||
@Override
|
||||
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
|
||||
builder.persistSpan(StrikethroughSpan.class, StrikethroughSpan::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMarkdownSpan(
|
||||
@NonNull PersistedSpans persistedSpans,
|
||||
@NonNull Editable editable,
|
||||
@NonNull String input,
|
||||
@NonNull StrikethroughSpan span,
|
||||
int spanStart,
|
||||
int spanTextLength) {
|
||||
final var match = MarkwonEditorUtils.findDelimited(input, spanStart, "~~");
|
||||
if (match != null) {
|
||||
editable.setSpan(
|
||||
persistedSpans.get(StrikethroughSpan.class),
|
||||
match.start(),
|
||||
match.end(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<StrikethroughSpan> markdownSpanType() {
|
||||
return StrikethroughSpan.class;
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.plugins;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.RequestManager;
|
||||
import com.bumptech.glide.request.target.Target;
|
||||
|
||||
import io.noties.markwon.image.AsyncDrawable;
|
||||
import io.noties.markwon.image.glide.GlideImagesPlugin;
|
||||
import it.niedermann.android.markdown.R;
|
||||
import it.niedermann.android.markdown.markwon.glide.DownsampleWithMaxWidth;
|
||||
|
||||
/**
|
||||
* <ul>
|
||||
* <li>Applies downscaling via {@link DownsampleWithMaxWidth} to avoid <a href="https://github.com/stefan-niedermann/nextcloud-notes/issues/1034">issues with large images</a></li>
|
||||
* <li>Adds a placeholder while loading an image</li>
|
||||
* <li>Adds a "broken image" placeholder in case of an error</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class CustomGlideStore implements GlideImagesPlugin.GlideStore {
|
||||
private final RequestManager requestManager;
|
||||
private final DownsampleWithMaxWidth downsampleWithMaxWidth;
|
||||
|
||||
public CustomGlideStore(@NonNull Context context) {
|
||||
this.requestManager = Glide.with(context);
|
||||
downsampleWithMaxWidth = new DownsampleWithMaxWidth(context.getResources().getDisplayMetrics().widthPixels);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RequestBuilder<Drawable> load(@NonNull AsyncDrawable drawable) {
|
||||
return requestManager
|
||||
.load(drawable.getDestination())
|
||||
.downsample(downsampleWithMaxWidth)
|
||||
.placeholder(R.drawable.ic_baseline_image_24)
|
||||
.error(R.drawable.ic_baseline_broken_image_24);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancel(@NonNull Target<?> target) {
|
||||
requestManager.clear(target);
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.plugins;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.commonmark.node.Link;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.function.Function;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.MarkwonPlugin;
|
||||
import io.noties.markwon.MarkwonSpansFactory;
|
||||
import io.noties.markwon.core.CoreProps;
|
||||
import it.niedermann.android.markdown.markwon.span.InterceptedURLSpan;
|
||||
|
||||
public class LinkClickInterceptorPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
@NonNull
|
||||
private final Collection<Function<String, Boolean>> onLinkClickCallbacks = new LinkedList<>();
|
||||
|
||||
public static MarkwonPlugin create() {
|
||||
return new LinkClickInterceptorPlugin();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
super.configureSpansFactory(builder);
|
||||
builder.setFactory(Link.class, (configuration, props) -> new InterceptedURLSpan(onLinkClickCallbacks, CoreProps.LINK_DESTINATION.get(props)));
|
||||
}
|
||||
|
||||
public void registerOnLinkClickCallback(@NonNull Function<String, Boolean> callback) {
|
||||
this.onLinkClickCallbacks.add(callback);
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.plugins;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
|
||||
import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
|
||||
import com.nextcloud.android.sso.helper.SingleAccountHelper;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.MarkwonPlugin;
|
||||
|
||||
import static it.niedermann.android.markdown.MentionUtil.setupMentions;
|
||||
|
||||
public class NextcloudMentionsPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
@NonNull
|
||||
private final Context context;
|
||||
@NonNull
|
||||
private final Map<String, String> mentions;
|
||||
|
||||
private NextcloudMentionsPlugin(@NonNull Context context, @NonNull Map<String, String> mentions) {
|
||||
this.context = context.getApplicationContext();
|
||||
this.mentions = mentions;
|
||||
}
|
||||
|
||||
public static MarkwonPlugin create(@NonNull Context context, @NonNull Map<String, String> mentions) {
|
||||
return new NextcloudMentionsPlugin(context, mentions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterSetText(@NonNull TextView textView) {
|
||||
super.afterSetText(textView);
|
||||
try {
|
||||
setupMentions(SingleAccountHelper.getCurrentSingleSignOnAccount(context), mentions, textView);
|
||||
} catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.plugins;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Spannable;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.MarkwonPlugin;
|
||||
import it.niedermann.android.markdown.MarkdownUtil;
|
||||
import it.niedermann.android.markdown.R;
|
||||
import it.niedermann.android.markdown.markwon.MarkwonMarkdownUtil;
|
||||
import it.niedermann.android.markdown.model.SearchSpan;
|
||||
|
||||
import static it.niedermann.android.markdown.MarkdownUtil.getContentAsSpannable;
|
||||
|
||||
public class SearchHighlightPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
@Nullable
|
||||
private CharSequence searchText = null;
|
||||
private Integer current;
|
||||
@ColorInt
|
||||
private int color;
|
||||
@ColorInt
|
||||
private final int highlightColor;
|
||||
private final boolean darkTheme;
|
||||
|
||||
public SearchHighlightPlugin(@NonNull Context context) {
|
||||
this.color = ContextCompat.getColor(context, R.color.search_color);
|
||||
this.highlightColor = ContextCompat.getColor(context, R.color.bg_highlighted);
|
||||
this.darkTheme = MarkwonMarkdownUtil.isDarkThemeActive(context);
|
||||
}
|
||||
|
||||
public static MarkwonPlugin create(@NonNull Context context) {
|
||||
return new SearchHighlightPlugin(context);
|
||||
}
|
||||
|
||||
public void setSearchText(@Nullable CharSequence searchText, @Nullable Integer current, @NonNull TextView textView) {
|
||||
this.current = current;
|
||||
MarkdownUtil.removeSpans(getContentAsSpannable(textView), SearchSpan.class);
|
||||
if (TextUtils.isEmpty(searchText)) {
|
||||
this.searchText = null;
|
||||
} else {
|
||||
this.searchText = searchText;
|
||||
afterSetText(textView);
|
||||
}
|
||||
}
|
||||
|
||||
public void setSearchColor(@ColorInt int color, @NonNull TextView textView) {
|
||||
this.color = color;
|
||||
afterSetText(textView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterSetText(@NonNull TextView textView) {
|
||||
super.afterSetText(textView);
|
||||
if (this.searchText != null) {
|
||||
final var spannable = getContentAsSpannable(textView);
|
||||
MarkdownUtil.searchAndColor(spannable, searchText, current, color, highlightColor, darkTheme);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.plugins;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.MarkwonPlugin;
|
||||
import io.noties.markwon.core.MarkwonTheme;
|
||||
import it.niedermann.android.markdown.R;
|
||||
|
||||
public class ThemePlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
@NonNull
|
||||
Context context;
|
||||
|
||||
private ThemePlugin(@NonNull Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public static MarkwonPlugin create(@NonNull Context context) {
|
||||
return new ThemePlugin(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureTheme(@NonNull MarkwonTheme.Builder builder) {
|
||||
super.configureTheme(builder);
|
||||
builder
|
||||
.headingBreakHeight(0)
|
||||
.codeBlockBackgroundColor(ContextCompat.getColor(context, R.color.bg_code))
|
||||
.headingTextSizeMultipliers(new float[]{1.45f, 1.35f, 1.25f, 1.15f, 1.1f, 1.05f})
|
||||
.bulletWidth(context.getResources().getDimensionPixelSize(R.dimen.bullet_point_width));
|
||||
}
|
||||
}
|
|
@ -1,251 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.plugins;
|
||||
|
||||
import static java.util.Comparator.comparingInt;
|
||||
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.util.Range;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import org.commonmark.node.AbstractVisitor;
|
||||
import org.commonmark.node.Block;
|
||||
import org.commonmark.node.HardLineBreak;
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.node.Paragraph;
|
||||
import org.commonmark.node.SoftLineBreak;
|
||||
import org.commonmark.node.Text;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.MarkwonVisitor;
|
||||
import io.noties.markwon.SpannableBuilder;
|
||||
import io.noties.markwon.SpannableBuilder.Span;
|
||||
import io.noties.markwon.ext.tasklist.TaskListItem;
|
||||
import io.noties.markwon.ext.tasklist.TaskListProps;
|
||||
import io.noties.markwon.ext.tasklist.TaskListSpan;
|
||||
import it.niedermann.android.markdown.MarkdownUtil;
|
||||
import it.niedermann.android.markdown.markwon.span.ToggleTaskListSpan;
|
||||
|
||||
/**
|
||||
* @see <a href="https://github.com/noties/Markwon/issues/196#issuecomment-751680138">Support from upstream</a>
|
||||
* @see <a href="https://github.com/noties/Markwon/blob/910bf311dac1bade400616a00ab0c9b7b7ade8cb/app-sample/src/main/java/io/noties/markwon/app/samples/tasklist/TaskListMutateNestedSample.kt">Original kotlin implementation</a>
|
||||
*/
|
||||
public class ToggleableTaskListPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
@NonNull
|
||||
private final AtomicBoolean enabled = new AtomicBoolean(true);
|
||||
@NonNull
|
||||
private final BiConsumer<Integer, Boolean> toggleListener;
|
||||
|
||||
public ToggleableTaskListPlugin(@NonNull BiConsumer<Integer, Boolean> toggleListener) {
|
||||
this.toggleListener = toggleListener;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled.set(enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares {@link TaskListSpan}s and marks each one with a {@link ToggleMarkerSpan} in the first step.
|
||||
* The {@link ToggleMarkerSpan} are different from {@link TaskListSpan}s as they will stop on nested tasks instead of spanning the whole tasks including its subtasks.
|
||||
*/
|
||||
@Override
|
||||
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
|
||||
builder.on(TaskListItem.class, (visitor, node) -> {
|
||||
final int length = visitor.length();
|
||||
visitor.visitChildren(node);
|
||||
TaskListProps.DONE.set(visitor.renderProps(), node.isDone());
|
||||
final var spanFactory = visitor.configuration()
|
||||
.spansFactory()
|
||||
.get(TaskListItem.class);
|
||||
final var spans = spanFactory == null ? null :
|
||||
spanFactory.getSpans(visitor.configuration(), visitor.renderProps());
|
||||
if (spans != null) {
|
||||
final TaskListSpan taskListSpan;
|
||||
if (spans instanceof TaskListSpan[]) {
|
||||
if (((TaskListSpan[]) spans).length > 0) {
|
||||
taskListSpan = ((TaskListSpan[]) spans)[0];
|
||||
} else {
|
||||
taskListSpan = null;
|
||||
}
|
||||
} else if (spans instanceof TaskListSpan) {
|
||||
taskListSpan = (TaskListSpan) spans;
|
||||
} else {
|
||||
taskListSpan = null;
|
||||
}
|
||||
|
||||
final int content = TaskListContextVisitor.contentLength(node);
|
||||
if (content > 0 && taskListSpan != null) {
|
||||
// maybe additionally identify this task list (for persistence)
|
||||
visitor.builder().setSpan(
|
||||
new ToggleMarkerSpan(taskListSpan),
|
||||
length,
|
||||
length + content
|
||||
);
|
||||
}
|
||||
}
|
||||
SpannableBuilder.setSpans(
|
||||
visitor.builder(),
|
||||
spans,
|
||||
length,
|
||||
visitor.length()
|
||||
);
|
||||
|
||||
if (visitor.hasNext(node)) {
|
||||
visitor.ensureNewLine();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds for each symbolic {@link ToggleMarkerSpan} an actual {@link ToggleTaskListSpan}s respecting existing {@link ClickableSpan}s.
|
||||
*/
|
||||
@Override
|
||||
public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) {
|
||||
super.afterRender(node, visitor);
|
||||
|
||||
final var markerSpans = getSortedSpans(visitor.builder(), ToggleMarkerSpan.class, 0, visitor.builder().length());
|
||||
|
||||
for (int position = 0; position < markerSpans.size(); position++) {
|
||||
final var markerSpan = markerSpans.get(position);
|
||||
final int start = markerSpan.start;
|
||||
final int end = markerSpan.end;
|
||||
final var freeRanges = findFreeRanges(visitor.builder(), start, end);
|
||||
for (Range<Integer> freeRange : freeRanges) {
|
||||
visitor.builder().setSpan(
|
||||
new ToggleTaskListSpan(enabled, toggleListener, ((ToggleMarkerSpan) markerSpan.what).getTaskListSpan(), position),
|
||||
freeRange.getLower(), freeRange.getUpper());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes {@link ToggleMarkerSpan}s from {@param textView}.
|
||||
*/
|
||||
@Override
|
||||
public void afterSetText(@NonNull TextView textView) {
|
||||
super.afterSetText(textView);
|
||||
final var spannable = MarkdownUtil.getContentAsSpannable(textView);
|
||||
for (final var span : spannable.getSpans(0, spannable.length(), ToggleMarkerSpan.class)) {
|
||||
spannable.removeSpan(span);
|
||||
}
|
||||
textView.setText(spannable);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a {@link List} of {@link Range}s in the given {@param spanned} from {@param start} to {@param end} which is <strong>not</strong> taken for a {@link ClickableSpan}.
|
||||
*/
|
||||
@NonNull
|
||||
private static Collection<Range<Integer>> findFreeRanges(@NonNull SpannableBuilder builder, int start, int end) {
|
||||
final List<Range<Integer>> freeRanges;
|
||||
final var clickableSpans = getSortedSpans(builder, ClickableSpan.class, start, end);
|
||||
if (clickableSpans.size() > 0) {
|
||||
freeRanges = new LinkedList<>();
|
||||
int from = start;
|
||||
for (final var clickableSpan : clickableSpans) {
|
||||
final int clickableStart = clickableSpan.start;
|
||||
final int clickableEnd = clickableSpan.end;
|
||||
if (from < clickableStart) {
|
||||
freeRanges.add(new Range<>(from, clickableStart));
|
||||
}
|
||||
from = clickableEnd;
|
||||
}
|
||||
if (clickableSpans.size() > 0) {
|
||||
final int lastUpperBlocker = clickableSpans.get(clickableSpans.size() - 1).end;
|
||||
if (lastUpperBlocker < end) {
|
||||
freeRanges.add(new Range<>(lastUpperBlocker, end));
|
||||
}
|
||||
}
|
||||
} else if (start == end) {
|
||||
freeRanges = Collections.emptyList();
|
||||
} else {
|
||||
freeRanges = Collections.singletonList(new Range<>(start, end));
|
||||
}
|
||||
return freeRanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a {@link List} of {@link Span}s holding {@param type}s, sorted ascending by the span start.
|
||||
*/
|
||||
private static <T> List<Span> getSortedSpans(@NonNull SpannableBuilder builder, @NonNull Class<T> type, int start, int end) {
|
||||
return builder.getSpans(start, end)
|
||||
.stream()
|
||||
.filter(span -> type.isInstance(span.what))
|
||||
.sorted(comparingInt(o -> o.start))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private static final class TaskListContextVisitor extends AbstractVisitor {
|
||||
private int contentLength = 0;
|
||||
|
||||
static int contentLength(Node node) {
|
||||
final var visitor = new TaskListContextVisitor();
|
||||
visitor.visitChildren(node);
|
||||
return visitor.contentLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(Text text) {
|
||||
super.visit(text);
|
||||
contentLength += text.getLiteral().length();
|
||||
}
|
||||
|
||||
// NB! if count both soft and hard breaks as having length of 1
|
||||
@Override
|
||||
public void visit(SoftLineBreak softLineBreak) {
|
||||
super.visit(softLineBreak);
|
||||
contentLength += 1;
|
||||
}
|
||||
|
||||
// NB! if count both soft and hard breaks as having length of 1
|
||||
@Override
|
||||
public void visit(HardLineBreak hardLineBreak) {
|
||||
super.visit(hardLineBreak);
|
||||
contentLength += 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void visitChildren(Node parent) {
|
||||
var node = parent.getFirstChild();
|
||||
while (node != null) {
|
||||
// A subclass of this visitor might modify the node, resulting in getNext returning a different node or no
|
||||
// node after visiting it. So get the next node before visiting.
|
||||
final var next = node.getNext();
|
||||
if (node instanceof Block && !(node instanceof Paragraph)) {
|
||||
break;
|
||||
}
|
||||
node.accept(this);
|
||||
node = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class which holds an {@link TaskListSpan} but does not include the range of child {@link TaskListSpan}s.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static final class ToggleMarkerSpan {
|
||||
|
||||
@NonNull
|
||||
private final TaskListSpan taskListSpan;
|
||||
|
||||
private ToggleMarkerSpan(@NonNull TaskListSpan taskListSpan) {
|
||||
this.taskListSpan = taskListSpan;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private TaskListSpan getTaskListSpan() {
|
||||
return taskListSpan;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.span;
|
||||
|
||||
import android.text.Spanned;
|
||||
import android.text.style.URLSpan;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.function.Function;
|
||||
|
||||
public class InterceptedURLSpan extends URLSpan {
|
||||
|
||||
private static final String TAG = InterceptedURLSpan.class.getSimpleName();
|
||||
@NonNull
|
||||
private final Collection<Function<String, Boolean>> onLinkClickCallbacks;
|
||||
private final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
|
||||
public InterceptedURLSpan(@NonNull Collection<Function<String, Boolean>> onLinkClickCallbacks, String url) {
|
||||
super(url);
|
||||
this.onLinkClickCallbacks = onLinkClickCallbacks;
|
||||
}
|
||||
@Override
|
||||
public void onClick(View widget) {
|
||||
if (onLinkClickCallbacks.size() > 0) {
|
||||
executor.submit(() -> {
|
||||
for (final var callback : onLinkClickCallbacks) {
|
||||
try {
|
||||
if (callback.apply(getURL())) {
|
||||
return;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, t.getMessage(), t);
|
||||
}
|
||||
}
|
||||
super.onClick(widget);
|
||||
});
|
||||
} else {
|
||||
super.onClick(widget);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.span;
|
||||
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import io.noties.markwon.ext.tasklist.TaskListSpan;
|
||||
|
||||
public class ToggleTaskListSpan extends ClickableSpan {
|
||||
|
||||
private static final String TAG = ToggleTaskListSpan.class.getSimpleName();
|
||||
|
||||
private final AtomicBoolean enabled;
|
||||
private final BiConsumer<Integer, Boolean> toggleListener;
|
||||
private final TaskListSpan span;
|
||||
private final int position;
|
||||
|
||||
public ToggleTaskListSpan(@NonNull AtomicBoolean enabled, @NonNull BiConsumer<Integer, Boolean> toggleListener, @NonNull TaskListSpan span, int position) {
|
||||
this.enabled = enabled;
|
||||
this.toggleListener = toggleListener;
|
||||
this.span = span;
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
if (enabled.get()) {
|
||||
span.setDone(!span.isDone());
|
||||
widget.invalidate();
|
||||
toggleListener.accept(position, span.isDone());
|
||||
} else {
|
||||
Log.w(TAG, "Prevented toggling checkbox because the view is disabled");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(@NonNull TextPaint ds) {
|
||||
// NoOp to remove underline text decoration
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.textwatcher;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import it.niedermann.android.markdown.markwon.MarkwonMarkdownEditor;
|
||||
import it.niedermann.android.markdown.model.EListType;
|
||||
|
||||
import static it.niedermann.android.markdown.MarkdownUtil.getEndOfLine;
|
||||
import static it.niedermann.android.markdown.MarkdownUtil.getListItemIfIsEmpty;
|
||||
import static it.niedermann.android.markdown.MarkdownUtil.getOrderedListNumber;
|
||||
import static it.niedermann.android.markdown.MarkdownUtil.getStartOfLine;
|
||||
import static it.niedermann.android.markdown.MarkdownUtil.lineStartsWithCheckbox;
|
||||
|
||||
/**
|
||||
* Automatically continues lists and checkbox lists when pressing enter
|
||||
*/
|
||||
public class AutoContinuationTextWatcher extends InterceptorTextWatcher {
|
||||
|
||||
@NonNull
|
||||
private final MarkwonMarkdownEditor editText;
|
||||
|
||||
private CharSequence customText = null;
|
||||
private CharSequence oldText = null;
|
||||
private boolean isInsert = true;
|
||||
private int sequenceStart = 0;
|
||||
private static final Pattern REGEX_WHITESPACES = Pattern.compile("^\\s*");
|
||||
|
||||
public AutoContinuationTextWatcher(@NonNull TextWatcher originalWatcher, @NonNull MarkwonMarkdownEditor editText) {
|
||||
super(originalWatcher);
|
||||
this.editText = editText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
if (count > 0) {
|
||||
final var inserted = getInsertedString(s, start, before, count);
|
||||
if (inserted.length() > 0 && inserted.charAt(inserted.length() - 1) == '\n') {
|
||||
handleNewlineInserted(s, start, count);
|
||||
}
|
||||
}
|
||||
oldText = s.toString();
|
||||
originalWatcher.onTextChanged(s, start, before, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (customText != null) {
|
||||
final var customText = this.customText;
|
||||
this.customText = null;
|
||||
if (isInsert) {
|
||||
insertCustomText(s, customText);
|
||||
} else {
|
||||
deleteCustomText(s, customText);
|
||||
}
|
||||
} else {
|
||||
originalWatcher.afterTextChanged(s);
|
||||
}
|
||||
editText.setMarkdownStringModel(s);
|
||||
}
|
||||
|
||||
private CharSequence getInsertedString(CharSequence newText, int start, int before, int count) {
|
||||
if (newText != null && newText.length() > (oldText == null ? 0 : oldText.length())) {
|
||||
// character added
|
||||
final int position = start + before;
|
||||
return newText.subSequence(position, position + count - before);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private void deleteCustomText(Editable s, CharSequence customText) {
|
||||
s.replace(sequenceStart, Math.min(sequenceStart + customText.length() + 1, s.length()), "\n");
|
||||
editText.setSelection(sequenceStart + 1);
|
||||
}
|
||||
|
||||
private void insertCustomText(Editable s, CharSequence customText) {
|
||||
s.insert(sequenceStart, customText);
|
||||
}
|
||||
|
||||
private void handleNewlineInserted(CharSequence originalSequence, int start, int count) {
|
||||
final var s = originalSequence.subSequence(0, originalSequence.length());
|
||||
final int startOfLine = getStartOfLine(s, start);
|
||||
final String line = s.subSequence(startOfLine, getEndOfLine(s, start)).toString();
|
||||
|
||||
final var emptyListString = getListItemIfIsEmpty(line);
|
||||
if (emptyListString.isPresent()) {
|
||||
customText = emptyListString.get();
|
||||
isInsert = false;
|
||||
sequenceStart = startOfLine;
|
||||
} else {
|
||||
final var builder = new StringBuilder();
|
||||
final var matcher = REGEX_WHITESPACES.matcher(line);
|
||||
if (matcher.find()) {
|
||||
builder.append(matcher.group());
|
||||
}
|
||||
final String trimmedLine = line.trim();
|
||||
for (final var listType : EListType.values()) {
|
||||
final boolean isCheckboxList = lineStartsWithCheckbox(trimmedLine, listType);
|
||||
final boolean isPlainList = !isCheckboxList && trimmedLine.startsWith(listType.listSymbolWithTrailingSpace);
|
||||
if (isPlainList || isCheckboxList) {
|
||||
builder.append(isPlainList ? listType.listSymbolWithTrailingSpace : listType.checkboxUncheckedWithTrailingSpace);
|
||||
customText = builder;
|
||||
isInsert = true;
|
||||
sequenceStart = start + count;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final var orderedListNumber = getOrderedListNumber(trimmedLine);
|
||||
if (orderedListNumber.isPresent()) {
|
||||
customText = builder.append(orderedListNumber.get() + 1).append(". ");
|
||||
isInsert = true;
|
||||
sequenceStart = start + count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.textwatcher;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import io.noties.markwon.editor.MarkwonEditor;
|
||||
import io.noties.markwon.editor.MarkwonEditorTextWatcher;
|
||||
import it.niedermann.android.markdown.markwon.MarkwonMarkdownEditor;
|
||||
|
||||
public class CombinedTextWatcher extends HashMap<Class<?>, TextWatcher> implements TextWatcher {
|
||||
|
||||
private final TextWatcher watcher;
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
public CombinedTextWatcher(@NonNull MarkwonEditor editor, @NonNull MarkwonMarkdownEditor editText) {
|
||||
put(MarkwonEditorTextWatcher.class, MarkwonEditorTextWatcher.withPreRender(editor, Executors.newSingleThreadExecutor(), editText));
|
||||
put(AutoContinuationTextWatcher.class, new AutoContinuationTextWatcher(get(MarkwonEditorTextWatcher.class), editText));
|
||||
put(LowerIndentionTextWatcher.class, new LowerIndentionTextWatcher(get(AutoContinuationTextWatcher.class), editText));
|
||||
put(SearchHighlightTextWatcher.class, new SearchHighlightTextWatcher(get(LowerIndentionTextWatcher.class), editText));
|
||||
watcher = get(SearchHighlightTextWatcher.class);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked"})
|
||||
@Nullable
|
||||
public <T> T get(@Nullable Class<T> key) {
|
||||
return (T) super.get(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
watcher.beforeTextChanged(s, start, count, after);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
watcher.onTextChanged(s, start, before, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
watcher.afterTextChanged(s);
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.textwatcher;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
abstract public class InterceptorTextWatcher implements TextWatcher {
|
||||
|
||||
@NonNull
|
||||
protected final TextWatcher originalWatcher;
|
||||
|
||||
public InterceptorTextWatcher(@NonNull TextWatcher originalWatcher) {
|
||||
this.originalWatcher = originalWatcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
this.originalWatcher.beforeTextChanged(s, start, count, after);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
this.originalWatcher.onTextChanged(s, start, before, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
this.originalWatcher.afterTextChanged(s);
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.textwatcher;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import it.niedermann.android.markdown.markwon.MarkwonMarkdownEditor;
|
||||
import it.niedermann.android.markdown.model.EListType;
|
||||
|
||||
import static it.niedermann.android.markdown.MarkdownUtil.getEndOfLine;
|
||||
import static it.niedermann.android.markdown.MarkdownUtil.getStartOfLine;
|
||||
|
||||
/**
|
||||
* Automatically lowers indention when pressing <kbd>Backspace</kbd> on lists and check lists
|
||||
*/
|
||||
public class LowerIndentionTextWatcher extends InterceptorTextWatcher {
|
||||
|
||||
@NonNull
|
||||
private final MarkwonMarkdownEditor editText;
|
||||
|
||||
private boolean backspacePressed = false;
|
||||
private int cursor = 0;
|
||||
|
||||
public LowerIndentionTextWatcher(@NonNull TextWatcher originalWatcher, @NonNull MarkwonMarkdownEditor editText) {
|
||||
super(originalWatcher);
|
||||
this.editText = editText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
if (count == 0 && before == 1) {
|
||||
if (editText.getSelectionStart() == editText.getSelectionEnd()) {
|
||||
backspacePressed = true;
|
||||
cursor = start;
|
||||
}
|
||||
}
|
||||
originalWatcher.onTextChanged(s, start, before, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
if (backspacePressed) {
|
||||
if (!handleBackspace(editable, cursor)) {
|
||||
originalWatcher.afterTextChanged(editable);
|
||||
}
|
||||
backspacePressed = false;
|
||||
} else {
|
||||
originalWatcher.afterTextChanged(editable);
|
||||
}
|
||||
editText.setMarkdownStringModel(editable);
|
||||
}
|
||||
|
||||
private boolean handleBackspace(@NonNull Editable editable, int cursor) {
|
||||
final int lineStart = getStartOfLine(editable, cursor);
|
||||
final int lineEnd = getEndOfLine(editable, cursor);
|
||||
|
||||
// The cursor must be at the end of the line to automatically continue
|
||||
if (cursor != lineEnd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final String line = editable.subSequence(lineStart, lineEnd).toString();
|
||||
final String trimmedLine = line.trim();
|
||||
|
||||
// There must be no content in this list item to automatically continue
|
||||
if ((line.indexOf(trimmedLine) + trimmedLine.length()) < line.length()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (final var listType : EListType.values()) {
|
||||
if (listType.listSymbol.equals(trimmedLine)) {
|
||||
if (trimmedLine.length() == EListType.DASH.listSymbol.length()) {
|
||||
return lowerIndention(editable, line, lineStart, lineEnd);
|
||||
}
|
||||
} else if (listType.checkboxUnchecked.equals(trimmedLine) || listType.checkboxChecked.equals(trimmedLine)) {
|
||||
if (trimmedLine.length() == EListType.DASH.checkboxUnchecked.length()) {
|
||||
return lowerIndention(editable, line, lineStart, lineEnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean lowerIndention(@NonNull Editable editable, String line, int lineStart, int lineEnd) {
|
||||
if (line.startsWith(" ")) {
|
||||
editable.insert(lineEnd, " ");
|
||||
editable.replace(lineStart, lineStart + 2, "");
|
||||
return true;
|
||||
} else if (line.startsWith(" ")) {
|
||||
editable.insert(lineEnd, " ");
|
||||
editable.replace(lineStart, lineStart + 1, "");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -1,67 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.textwatcher;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import it.niedermann.android.markdown.MarkdownUtil;
|
||||
import it.niedermann.android.markdown.R;
|
||||
import it.niedermann.android.markdown.markwon.MarkwonMarkdownEditor;
|
||||
import it.niedermann.android.markdown.markwon.MarkwonMarkdownUtil;
|
||||
import it.niedermann.android.markdown.model.SearchSpan;
|
||||
|
||||
public class SearchHighlightTextWatcher extends InterceptorTextWatcher {
|
||||
|
||||
private final MarkwonMarkdownEditor editText;
|
||||
@Nullable
|
||||
private CharSequence searchText;
|
||||
private Integer current;
|
||||
@ColorInt
|
||||
private int color;
|
||||
@ColorInt
|
||||
private final int highlightColor;
|
||||
private final boolean darkTheme;
|
||||
|
||||
public SearchHighlightTextWatcher(@NonNull TextWatcher originalWatcher, @NonNull MarkwonMarkdownEditor editText) {
|
||||
super(originalWatcher);
|
||||
this.editText = editText;
|
||||
final var context = editText.getContext();
|
||||
this.color = ContextCompat.getColor(context, R.color.search_color);
|
||||
this.highlightColor = ContextCompat.getColor(context, R.color.bg_highlighted);
|
||||
this.darkTheme = MarkwonMarkdownUtil.isDarkThemeActive(context);
|
||||
}
|
||||
|
||||
public void setSearchText(@Nullable CharSequence searchText, @Nullable Integer current) {
|
||||
this.current = current;
|
||||
if (TextUtils.isEmpty(searchText)) {
|
||||
this.searchText = null;
|
||||
final var text = editText.getText();
|
||||
if (text != null) {
|
||||
MarkdownUtil.removeSpans(text, SearchSpan.class);
|
||||
}
|
||||
} else {
|
||||
this.searchText = searchText;
|
||||
afterTextChanged(editText.getText());
|
||||
}
|
||||
}
|
||||
|
||||
public void setSearchColor(@ColorInt int color) {
|
||||
this.color = color;
|
||||
afterTextChanged(editText.getText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
originalWatcher.afterTextChanged(s);
|
||||
if (searchText != null) {
|
||||
MarkdownUtil.removeSpans(s, SearchSpan.class);
|
||||
MarkdownUtil.searchAndColor(s, searchText, current, color, highlightColor, darkTheme);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package it.niedermann.android.markdown.model;
|
||||
|
||||
public enum EListType {
|
||||
STAR('*'),
|
||||
DASH('-'),
|
||||
PLUS('+');
|
||||
|
||||
public final String listSymbol;
|
||||
public final String listSymbolWithTrailingSpace;
|
||||
public final String checkboxChecked;
|
||||
public final String checkboxCheckedUpperCase;
|
||||
public final String checkboxUnchecked;
|
||||
public final String checkboxUncheckedWithTrailingSpace;
|
||||
|
||||
EListType(char listSymbol) {
|
||||
this.listSymbol = String.valueOf(listSymbol);
|
||||
this.listSymbolWithTrailingSpace = listSymbol + " ";
|
||||
this.checkboxChecked = listSymbolWithTrailingSpace + "[x]";
|
||||
this.checkboxCheckedUpperCase = listSymbolWithTrailingSpace + "[X]";
|
||||
this.checkboxUnchecked = listSymbolWithTrailingSpace + "[ ]";
|
||||
this.checkboxUncheckedWithTrailingSpace = checkboxUnchecked + " ";
|
||||
}
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
package it.niedermann.android.markdown.model;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.MetricAffectingSpan;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import it.niedermann.android.util.ColorUtil;
|
||||
|
||||
public class SearchSpan extends MetricAffectingSpan {
|
||||
|
||||
private final boolean current;
|
||||
@ColorInt
|
||||
private final int mainColor;
|
||||
@ColorInt
|
||||
private final int highlightColor;
|
||||
private final boolean darkTheme;
|
||||
|
||||
public SearchSpan(@ColorInt int mainColor, @ColorInt int highlightColor, boolean current, boolean darkTheme) {
|
||||
this.mainColor = mainColor;
|
||||
this.current = current;
|
||||
this.highlightColor = highlightColor;
|
||||
this.darkTheme = darkTheme;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(TextPaint tp) {
|
||||
if (current) {
|
||||
if (darkTheme) {
|
||||
if (ColorUtil.INSTANCE.isColorDark(mainColor)) {
|
||||
tp.bgColor = Color.WHITE;
|
||||
tp.setColor(mainColor);
|
||||
} else {
|
||||
tp.bgColor = mainColor;
|
||||
tp.setColor(Color.BLACK);
|
||||
}
|
||||
} else {
|
||||
if (ColorUtil.INSTANCE.isColorDark(mainColor)) {
|
||||
tp.bgColor = mainColor;
|
||||
tp.setColor(Color.WHITE);
|
||||
} else {
|
||||
if (ColorUtil.INSTANCE.getContrastRatio(mainColor, highlightColor) > 3d) {
|
||||
tp.bgColor = highlightColor;
|
||||
} else {
|
||||
tp.bgColor = Color.BLACK;
|
||||
}
|
||||
tp.setColor(mainColor);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tp.bgColor = highlightColor;
|
||||
if (ColorUtil.INSTANCE.getContrastRatio(mainColor, highlightColor) > 3d) {
|
||||
tp.setColor(mainColor);
|
||||
} else {
|
||||
if (darkTheme) {
|
||||
tp.setColor(Color.WHITE);
|
||||
} else {
|
||||
tp.setColor(Color.BLACK);
|
||||
}
|
||||
}
|
||||
}
|
||||
tp.setFakeBoldText(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateMeasureState(@NonNull TextPaint tp) {
|
||||
tp.setFakeBoldText(true);
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#757575"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M21,5v6.59l-3,-3.01 -4,4.01 -4,-4 -4,4 -3,-3.01L3,5c0,-1.1 0.9,-2 2,-2h14c1.1,0 2,0.9 2,2zM18,11.42l3,3.01L21,19c0,1.1 -0.9,2 -2,2L5,21c-1.1,0 -2,-0.9 -2,-2v-6.58l3,2.99 4,-4 4,4 4,-3.99z"/>
|
||||
</vector>
|
|
@ -1,5 +0,0 @@
|
|||
<vector android:height="24dp" android:tint="#757575"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
|
||||
</vector>
|
|
@ -1,11 +0,0 @@
|
|||
<vector android:autoMirrored="true"
|
||||
android:height="24dp"
|
||||
android:tint="#757575"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0"
|
||||
android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M15.6,10.79c0.97,-0.67 1.65,-1.77 1.65,-2.79 0,-2.26 -1.75,-4 -4,-4L7,4v14h7.04c2.09,0 3.71,-1.7 3.71,-3.79 0,-1.52 -0.86,-2.82 -2.15,-3.42zM10,6.5h3c0.83,0 1.5,0.67 1.5,1.5s-0.67,1.5 -1.5,1.5h-3v-3zM13.5,15.5L10,15.5v-3h3.5c0.83,0 1.5,0.67 1.5,1.5s-0.67,1.5 -1.5,1.5z" />
|
||||
</vector>
|
|
@ -1,11 +0,0 @@
|
|||
<vector android:autoMirrored="true"
|
||||
android:height="24dp"
|
||||
android:tint="#757575"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0"
|
||||
android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M10,4v3h2.21l-3.42,8H6v3h8v-3h-2.21l3.42,-8H18V4z" />
|
||||
</vector>
|
|
@ -1,11 +0,0 @@
|
|||
<vector android:autoMirrored="true"
|
||||
android:height="24dp"
|
||||
android:tint="#757575"
|
||||
android:viewportHeight="24.0"
|
||||
android:viewportWidth="24.0"
|
||||
android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4L11,7L7,7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9L7,15.1c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2L8,11v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4L13,17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z" />
|
||||
</vector>
|
|
@ -1,10 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#757575"
|
||||
android:pathData="M12,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM12,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z" />
|
||||
</vector>
|
|
@ -1,13 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/checkbox"
|
||||
android:title="@string/simple_checkbox"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/link"
|
||||
android:icon="@drawable/ic_insert_link_black_24dp"
|
||||
android:title="@string/simple_link"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
|
@ -1,19 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/italic"
|
||||
android:icon="@drawable/ic_format_italic_black_24dp"
|
||||
android:title="@string/simple_italic"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/bold"
|
||||
android:icon="@drawable/ic_format_bold_black_24dp"
|
||||
android:title="@string/simple_bold"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/link"
|
||||
android:icon="@drawable/ic_insert_link_black_24dp"
|
||||
android:title="@string/simple_link"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="bg_highlighted">#2a2a2a</color>
|
||||
<color name="bg_code">#11ffffff</color>
|
||||
<color name="block_quote">#666</color>
|
||||
</resources>
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="search_color">#0082C9</color>
|
||||
<color name="bg_highlighted">#eee</color>
|
||||
<color name="bg_code">#0e000000</color>
|
||||
<color name="block_quote">#aaa</color>
|
||||
</resources>
|
|
@ -1,4 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="bullet_point_width">6dp</dimen>
|
||||
</resources>
|
|
@ -1,7 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="simple_checkbox">Checkbox</string>
|
||||
<string name="simple_bold">Bold</string>
|
||||
<string name="simple_link">Link</string>
|
||||
<string name="simple_italic">Italic</string>
|
||||
</resources>
|
|
@ -1,142 +0,0 @@
|
|||
package it.niedermann.android.markdown;
|
||||
|
||||
import androidx.core.text.HtmlCompat;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class ListTagHandlerTest extends TestCase {
|
||||
|
||||
//language=html
|
||||
private static final String SAMPLE_HTML_WIDGET_TEST = "<h1>Widget-Test</h1>" +
|
||||
"<p><a href=\"https://nextcloud.com\">Link</a></p>" +
|
||||
"<p><strong>bold</strong> <em>italic</em> <em><strong>itabold</strong></em> ~~strike~~</p>" +
|
||||
"<ol start=\"3\">" +
|
||||
"<li>Item" +
|
||||
"<ol>" +
|
||||
"<li>Subitem</li>" +
|
||||
"<li>Subitem" +
|
||||
"<ul>" +
|
||||
"<li>Subi</li>" +
|
||||
"<li>Subi</li>" +
|
||||
"</ul>" +
|
||||
"</li>" +
|
||||
"<li>Test</li>" +
|
||||
"</ol>" +
|
||||
"</li>" +
|
||||
"<li>Item</li>" +
|
||||
"<li>Item</li>" +
|
||||
"</ol>" +
|
||||
"<ul>" +
|
||||
"<li>Unordered</li>" +
|
||||
"<li>Unordered</li>" +
|
||||
"</ul>" +
|
||||
"<p>☐ Unchecked<br>☒ Checked</p>";
|
||||
|
||||
//language=html
|
||||
private static final String SAMPLE_HTML_MARKDOWN_SYNTAX =
|
||||
"<h2>This is a header.</h2>" +
|
||||
"<ol>" +
|
||||
"<li>This is the first list item.</li>" +
|
||||
"<li>This is the second list item.</li>" +
|
||||
"</ol>" +
|
||||
"<p>Here's some example code:</p>" +
|
||||
"<pre><code>return shell_exec("echo $input | $markdown_script");" +
|
||||
"</code></pre>" +
|
||||
"</blockquote>" +
|
||||
"<p>Any decent text editor should make email-style quoting easy. For<br>example, with BBEdit, you can make a selection and choose Increase<br>Quote Level from the Text menu.</p>" +
|
||||
"<h3>Lists</h3>" +
|
||||
"<p>Markdown supports ordered (numbered) and unordered (bulleted) lists.</p>" +
|
||||
"<p>Unordered lists use asterisks, pluses, and hyphens -- interchangably<br>-- as list markers:</p>" +
|
||||
"<ul>" +
|
||||
"<li>Red</li>" +
|
||||
"<li>Green</li>" +
|
||||
"<li>Blue</li>" +
|
||||
"</ul>" +
|
||||
"<p>is equivalent to:</p>" +
|
||||
"<ul>" +
|
||||
"<li>Red</li>" +
|
||||
"<li>Green</li>" +
|
||||
"<li>Blue</li>" +
|
||||
"</ul>" +
|
||||
"<p>and:</p>" +
|
||||
"<ul>" +
|
||||
"<li>Red</li>" +
|
||||
"<li>Green</li>" +
|
||||
"<li>Blue</li>" +
|
||||
"</ul>" +
|
||||
"<p>Ordered lists use numbers followed by periods:</p>" +
|
||||
"<ol>" +
|
||||
"<li>Bird</li>" +
|
||||
"<li>McHale</li>" +
|
||||
"<li>Parish</li>" +
|
||||
"</ol>" +
|
||||
"<p>It's important to note that the actual numbers you use to mark the<br>list have no effect on the HTML output Markdown produces. The HTML<br>Markdown produces from the above list is:</p>" +
|
||||
"<p>If you instead wrote the list in Markdown like this:</p>" +
|
||||
"<ol>" +
|
||||
"<li>Bird</li>" +
|
||||
"<li>McHale</li>" +
|
||||
"<li>Parish</li>" +
|
||||
"</ol>" +
|
||||
"<p>or even:</p>" +
|
||||
"<ol start=\"3\">" +
|
||||
"<li>Bird</li>" +
|
||||
"<li>McHale</li>" +
|
||||
"<li>Parish</li>" +
|
||||
"</ol>";
|
||||
|
||||
@Test
|
||||
public void testMarkOrderedListTags() {
|
||||
assertTrue(SAMPLE_HTML_WIDGET_TEST.contains("<ol start=\"3\">"));
|
||||
assertTrue(SAMPLE_HTML_WIDGET_TEST.contains("</ol>"));
|
||||
assertTrue(SAMPLE_HTML_WIDGET_TEST.contains("</ul>"));
|
||||
assertTrue(SAMPLE_HTML_WIDGET_TEST.contains("</ul>"));
|
||||
assertTrue(SAMPLE_HTML_WIDGET_TEST.contains("</li>"));
|
||||
assertTrue(SAMPLE_HTML_WIDGET_TEST.contains("</li>"));
|
||||
|
||||
final String markedSampleHtml = ListTagHandler.prepareTagHandling(SAMPLE_HTML_WIDGET_TEST);
|
||||
|
||||
assertFalse(markedSampleHtml.contains("<ol start=\"3\">"));
|
||||
assertFalse(markedSampleHtml.contains("</ol>"));
|
||||
assertFalse(markedSampleHtml.contains("</ul>"));
|
||||
assertFalse(markedSampleHtml.contains("</ul>"));
|
||||
assertFalse(markedSampleHtml.contains("</li>"));
|
||||
assertFalse(markedSampleHtml.contains("</li>"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHandleTag() {
|
||||
final var handler = new ListTagHandler();
|
||||
|
||||
assertEquals("\n• Item ", HtmlCompat.fromHtml(ListTagHandler.prepareTagHandling("<ul><li>Item</li></ul>"), 0, null, handler).toString());
|
||||
|
||||
final String[] lines = HtmlCompat.fromHtml(ListTagHandler.prepareTagHandling(SAMPLE_HTML_WIDGET_TEST), 0, null, handler).toString().split("\n");
|
||||
|
||||
assertEquals("Widget-Test", lines[0]);
|
||||
assertEquals("", lines[1]);
|
||||
assertEquals("Link", lines[2]);
|
||||
assertEquals("", lines[3]);
|
||||
assertEquals("bold italic itabold ~~strike~~", lines[4]);
|
||||
assertEquals("", lines[5]);
|
||||
assertEquals("", lines[6]);
|
||||
assertEquals("1. Item ", lines[7]);
|
||||
assertEquals("\t\t1. Subitem ", lines[8]);
|
||||
assertEquals("\t\t2. Subitem ", lines[9]);
|
||||
assertEquals("\t\t\t\t• Subi ", lines[10]);
|
||||
assertEquals("\t\t\t\t• Subi ", lines[11]);
|
||||
assertEquals("\t\t3. Test ", lines[12]);
|
||||
assertEquals("2. Item ", lines[13]);
|
||||
assertEquals("3. Item ", lines[14]);
|
||||
assertEquals("• Unordered ", lines[15]);
|
||||
assertEquals("• Unordered ", lines[16]);
|
||||
assertEquals("", lines[17]);
|
||||
assertEquals("☐ Unchecked", lines[18]);
|
||||
assertEquals("☒ Checked", lines[19]);
|
||||
|
||||
HtmlCompat.fromHtml(ListTagHandler.prepareTagHandling(SAMPLE_HTML_MARKDOWN_SYNTAX), 0, null, handler);
|
||||
}
|
||||
}
|
|
@ -1,829 +0,0 @@
|
|||
package it.niedermann.android.markdown;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.text.Editable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import it.niedermann.android.markdown.model.EListType;
|
||||
import it.niedermann.android.markdown.model.SearchSpan;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class MarkdownUtilTest extends TestCase {
|
||||
|
||||
@Test
|
||||
public void testGetStartOfLine() {
|
||||
//language=md
|
||||
final var test = new StringBuilder(
|
||||
"# Test-Note\n" + // line start 0
|
||||
"\n" + // line start 12
|
||||
"- [ ] this is a test note\n" + // line start 13
|
||||
"- [x] test\n" + // line start 39
|
||||
"[test](https://example.com)\n" + // line start 50
|
||||
"\n" + // line start 77
|
||||
"\n" // line start 78
|
||||
);
|
||||
|
||||
for (int i = 0; i < test.length(); i++) {
|
||||
int startOfLine = MarkdownUtil.getStartOfLine(test, i);
|
||||
if (i <= 11) {
|
||||
assertEquals(0, startOfLine);
|
||||
} else if (i <= 12) {
|
||||
assertEquals(12, startOfLine);
|
||||
} else if (i <= 38) {
|
||||
assertEquals(13, startOfLine);
|
||||
} else if (i <= 49) {
|
||||
assertEquals(39, startOfLine);
|
||||
} else if (i <= 77) {
|
||||
assertEquals(50, startOfLine);
|
||||
} else if (i <= 78) {
|
||||
assertEquals(78, startOfLine);
|
||||
} else if (i <= 79) {
|
||||
assertEquals(79, startOfLine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetEndOfLine() {
|
||||
//language=md
|
||||
final var test = "# Test-Note\n" + // line 0 - 11
|
||||
"\n" + // line 12 - 12
|
||||
"- [ ] this is a test note\n" + // line 13 - 38
|
||||
"- [x] test\n" + // line start 39 - 49
|
||||
"[test](https://example.com)\n" + // line 50 - 77
|
||||
"\n" + // line 77 - 78
|
||||
"\n"; // line 78 - 79
|
||||
|
||||
for (int i = 0; i < test.length(); i++) {
|
||||
int endOfLine = MarkdownUtil.getEndOfLine(test, i);
|
||||
if (i <= 11) {
|
||||
assertEquals(11, endOfLine);
|
||||
} else if (i <= 12) {
|
||||
assertEquals(12, endOfLine);
|
||||
} else if (i <= 38) {
|
||||
assertEquals(38, endOfLine);
|
||||
} else if (i <= 49) {
|
||||
assertEquals(49, endOfLine);
|
||||
} else if (i <= 77) {
|
||||
assertEquals(77, endOfLine);
|
||||
} else if (i <= 78) {
|
||||
assertEquals(78, endOfLine);
|
||||
} else if (i <= 79) {
|
||||
assertEquals(79, endOfLine);
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals(3, MarkdownUtil.getEndOfLine(" - ", 2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetMarkdownLink() {
|
||||
assertEquals("[Foo](https://bar)", MarkdownUtil.getMarkdownLink("Foo", "https://bar"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLineStartsWithCheckbox() {
|
||||
final var lines = new HashMap<String, Boolean>();
|
||||
lines.put(" - [ ] a", true);
|
||||
lines.put(" - [x] a", true);
|
||||
lines.put(" - [X] a", true);
|
||||
lines.put(" * [ ] a", true);
|
||||
lines.put(" * [x] a", true);
|
||||
lines.put(" * [X] a", true);
|
||||
lines.put(" + [ ] a", true);
|
||||
lines.put(" + [x] a", true);
|
||||
lines.put(" + [X] a", true);
|
||||
lines.put("- [ ] a", true);
|
||||
lines.put("- [x] a", true);
|
||||
lines.put("- [X] a", true);
|
||||
lines.put("* [ ] a", true);
|
||||
lines.put("* [x] a", true);
|
||||
lines.put("* [X] a", true);
|
||||
lines.put("+ [ ] a", true);
|
||||
lines.put("+ [x] a", true);
|
||||
lines.put("+ [X] a", true);
|
||||
lines.put(" - [ ] ", true);
|
||||
lines.put(" - [x] ", true);
|
||||
lines.put(" - [X] ", true);
|
||||
lines.put(" * [ ] ", true);
|
||||
lines.put(" * [x] ", true);
|
||||
lines.put(" * [X] ", true);
|
||||
lines.put(" + [ ] ", true);
|
||||
lines.put(" + [x] ", true);
|
||||
lines.put(" + [X] ", true);
|
||||
lines.put(" - [ ]", true);
|
||||
lines.put(" - [x]", true);
|
||||
lines.put(" - [X]", true);
|
||||
lines.put(" * [ ]", true);
|
||||
lines.put(" * [x]", true);
|
||||
lines.put(" * [X]", true);
|
||||
lines.put(" + [ ]", true);
|
||||
lines.put(" + [x]", true);
|
||||
lines.put(" + [X]", true);
|
||||
lines.put("- [ ] ", true);
|
||||
lines.put("- [x] ", true);
|
||||
lines.put("- [X] ", true);
|
||||
lines.put("* [ ] ", true);
|
||||
lines.put("* [x] ", true);
|
||||
lines.put("* [X] ", true);
|
||||
lines.put("+ [ ] ", true);
|
||||
lines.put("+ [x] ", true);
|
||||
lines.put("+ [X] ", true);
|
||||
lines.put("- [ ]", true);
|
||||
lines.put("- [x]", true);
|
||||
lines.put("- [X]", true);
|
||||
lines.put("* [ ]", true);
|
||||
lines.put("* [x]", true);
|
||||
lines.put("* [X]", true);
|
||||
lines.put("+ [ ]", true);
|
||||
lines.put("+ [x]", true);
|
||||
lines.put("+ [X]", true);
|
||||
|
||||
lines.put("-[ ] ", false);
|
||||
lines.put("-[x] ", false);
|
||||
lines.put("-[X] ", false);
|
||||
lines.put("*[ ] ", false);
|
||||
lines.put("*[x] ", false);
|
||||
lines.put("*[X] ", false);
|
||||
lines.put("+[ ] ", false);
|
||||
lines.put("+[x] ", false);
|
||||
lines.put("+[X] ", false);
|
||||
lines.put("-[ ]", false);
|
||||
lines.put("-[x]", false);
|
||||
lines.put("-[X]", false);
|
||||
lines.put("*[ ]", false);
|
||||
lines.put("*[x]", false);
|
||||
lines.put("*[X]", false);
|
||||
lines.put("+[ ]", false);
|
||||
lines.put("+[x]", false);
|
||||
lines.put("+[X]", false);
|
||||
|
||||
lines.put("- [] ", false);
|
||||
lines.put("* [] ", false);
|
||||
lines.put("+ [] ", false);
|
||||
lines.put("- []", false);
|
||||
lines.put("* []", false);
|
||||
lines.put("+ []", false);
|
||||
|
||||
lines.put("-[] ", false);
|
||||
lines.put("*[] ", false);
|
||||
lines.put("+[] ", false);
|
||||
lines.put("-[]", false);
|
||||
lines.put("*[]", false);
|
||||
lines.put("+[]", false);
|
||||
|
||||
lines.forEach((key, value) -> assertEquals(value, (Boolean) MarkdownUtil.lineStartsWithCheckbox(key)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTogglePunctuation() {
|
||||
Editable builder;
|
||||
|
||||
// Add italic
|
||||
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
|
||||
assertEquals(12, MarkdownUtil.togglePunctuation(builder, 6, 11, "*"));
|
||||
assertEquals("Lorem *ipsum* dolor sit amet.", builder.toString());
|
||||
|
||||
// Remove italic
|
||||
builder = new SpannableStringBuilder("Lorem *ipsum* dolor sit amet.");
|
||||
assertEquals(11, MarkdownUtil.togglePunctuation(builder, 7, 12, "*"));
|
||||
assertEquals("Lorem ipsum dolor sit amet.", builder.toString());
|
||||
|
||||
// Add bold
|
||||
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
|
||||
assertEquals(13, MarkdownUtil.togglePunctuation(builder, 6, 11, "**"));
|
||||
assertEquals("Lorem **ipsum** dolor sit amet.", builder.toString());
|
||||
|
||||
// Remove bold
|
||||
builder = new SpannableStringBuilder("Lorem **ipsum** dolor sit amet.");
|
||||
assertEquals(11, MarkdownUtil.togglePunctuation(builder, 8, 13, "**"));
|
||||
assertEquals("Lorem ipsum dolor sit amet.", builder.toString());
|
||||
|
||||
// Add strike
|
||||
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
|
||||
assertEquals(13, MarkdownUtil.togglePunctuation(builder, 6, 11, "~~"));
|
||||
assertEquals("Lorem ~~ipsum~~ dolor sit amet.", builder.toString());
|
||||
|
||||
// Remove strike
|
||||
builder = new SpannableStringBuilder("Lorem ~~ipsum~~ dolor sit amet.");
|
||||
assertEquals(11, MarkdownUtil.togglePunctuation(builder, 8, 13, "~~"));
|
||||
assertEquals("Lorem ipsum dolor sit amet.", builder.toString());
|
||||
|
||||
// Add italic at first position
|
||||
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
|
||||
assertEquals(6, MarkdownUtil.togglePunctuation(builder, 0, 5, "*"));
|
||||
assertEquals("*Lorem* ipsum dolor sit amet.", builder.toString());
|
||||
|
||||
// Remove italic from first position
|
||||
builder = new SpannableStringBuilder("*Lorem* ipsum dolor sit amet.");
|
||||
assertEquals(5, MarkdownUtil.togglePunctuation(builder, 1, 6, "*"));
|
||||
assertEquals("Lorem ipsum dolor sit amet.", builder.toString());
|
||||
|
||||
// Add italic at last position
|
||||
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
|
||||
assertEquals(28, MarkdownUtil.togglePunctuation(builder, 22, 27, "*"));
|
||||
assertEquals("Lorem ipsum dolor sit *amet.*", builder.toString());
|
||||
|
||||
// Remove italic from last position
|
||||
builder = new SpannableStringBuilder("Lorem ipsum dolor sit *amet.*");
|
||||
assertEquals(27, MarkdownUtil.togglePunctuation(builder, 23, 28, "*"));
|
||||
assertEquals("Lorem ipsum dolor sit amet.", builder.toString());
|
||||
|
||||
// Text is not directly surrounded by punctuation but contains it
|
||||
|
||||
// Do nothing when the same punctuation is contained only one time
|
||||
builder = new SpannableStringBuilder("Lorem *ipsum dolor sit amet.");
|
||||
assertEquals(28, MarkdownUtil.togglePunctuation(builder, 0, 28, "*"));
|
||||
assertEquals("Lorem *ipsum dolor sit amet.", builder.toString());
|
||||
|
||||
// Do nothing when the same punctuation is contained only one time
|
||||
builder = new SpannableStringBuilder("Lorem **ipsum dolor sit amet.");
|
||||
assertEquals(29, MarkdownUtil.togglePunctuation(builder, 0, 29, "**"));
|
||||
assertEquals("Lorem **ipsum dolor sit amet.", builder.toString());
|
||||
|
||||
// Remove containing punctuation
|
||||
builder = new SpannableStringBuilder("Lorem *ipsum* dolor sit amet.");
|
||||
assertEquals(11, MarkdownUtil.togglePunctuation(builder, 6, 13, "*"));
|
||||
assertEquals("Lorem ipsum dolor sit amet.", builder.toString());
|
||||
|
||||
// Remove containing punctuation
|
||||
builder = new SpannableStringBuilder("Lorem *ipsum* dolor sit amet.");
|
||||
assertEquals(27, MarkdownUtil.togglePunctuation(builder, 0, 29, "*"));
|
||||
assertEquals("Lorem ipsum dolor sit amet.", builder.toString());
|
||||
|
||||
// Remove multiple containing punctuations
|
||||
builder = new SpannableStringBuilder("Lorem *ipsum* dolor *sit* amet.");
|
||||
assertEquals(27, MarkdownUtil.togglePunctuation(builder, 0, 31, "*"));
|
||||
assertEquals("Lorem ipsum dolor sit amet.", builder.toString());
|
||||
|
||||
// Toggle italic on bold text
|
||||
builder = new SpannableStringBuilder("Lorem **ipsum** dolor sit amet.");
|
||||
assertEquals(14, MarkdownUtil.togglePunctuation(builder, 8, 13, "*"));
|
||||
assertEquals("Lorem ***ipsum*** dolor sit amet.", builder.toString());
|
||||
|
||||
// Toggle italic on bold text
|
||||
builder = new SpannableStringBuilder("Lorem **ipsum** dolor sit amet.");
|
||||
assertEquals(16, MarkdownUtil.togglePunctuation(builder, 6, 15, "*"));
|
||||
assertEquals("Lorem ***ipsum*** dolor sit amet.", builder.toString());
|
||||
|
||||
// Toggle bold on italic text
|
||||
builder = new SpannableStringBuilder("Lorem *ipsum* dolor sit amet.");
|
||||
assertEquals(14, MarkdownUtil.togglePunctuation(builder, 7, 12, "**"));
|
||||
assertEquals("Lorem ***ipsum*** dolor sit amet.", builder.toString());
|
||||
|
||||
// Toggle bold to italic
|
||||
builder = new SpannableStringBuilder("Lorem **ipsum** dolor sit amet.");
|
||||
assertEquals(32, MarkdownUtil.togglePunctuation(builder, 0, 31, "*"));
|
||||
assertEquals("*Lorem **ipsum** dolor sit amet.*", builder.toString());
|
||||
|
||||
// Toggle italic and bold to bold
|
||||
builder = new SpannableStringBuilder("Lorem ***ipsum*** dolor sit amet.");
|
||||
assertEquals(13, MarkdownUtil.togglePunctuation(builder, 0, 14, "*"));
|
||||
assertEquals("Lorem **ipsum** dolor sit amet.", builder.toString());
|
||||
|
||||
// toggle italic around multiple existing bolds
|
||||
builder = new SpannableStringBuilder("Lorem **ipsum** dolor **sit** amet.");
|
||||
assertEquals(35, MarkdownUtil.togglePunctuation(builder, 0, 34, "*"));
|
||||
assertEquals("*Lorem **ipsum** dolor **sit** amet*.", builder.toString());
|
||||
|
||||
// Toggle italic and bold to italic
|
||||
builder = new SpannableStringBuilder("Lorem ***ipsum*** dolor sit amet.");
|
||||
assertEquals(12, MarkdownUtil.togglePunctuation(builder, 9, 14, "**"));
|
||||
assertEquals("Lorem *ipsum* dolor sit amet.", builder.toString());
|
||||
|
||||
// Toggle multiple italic and bold to bold
|
||||
builder = new SpannableStringBuilder("Lorem ***ipsum*** dolor ***sit*** amet.");
|
||||
assertEquals(34, MarkdownUtil.togglePunctuation(builder, 0, 38, "*"));
|
||||
assertEquals("Lorem **ipsum** dolor **sit** amet.", builder.toString());
|
||||
|
||||
// Toggle multiple italic and bold to italic
|
||||
builder = new SpannableStringBuilder("Lorem ***ipsum*** dolor ***sit*** amet.");
|
||||
assertEquals(30, MarkdownUtil.togglePunctuation(builder, 0, 38, "**"));
|
||||
assertEquals("Lorem *ipsum* dolor *sit* amet.", builder.toString());
|
||||
|
||||
// Toggle italic on an empty text
|
||||
builder = new SpannableStringBuilder("");
|
||||
assertEquals(1, MarkdownUtil.togglePunctuation(builder, 0, 0, "*"));
|
||||
assertEquals("**", builder.toString());
|
||||
|
||||
// Toggle italic on a blank selection
|
||||
builder = new SpannableStringBuilder(" ");
|
||||
assertEquals(2, MarkdownUtil.togglePunctuation(builder, 0, 1, "*"));
|
||||
assertEquals("* *", builder.toString());
|
||||
|
||||
// Toggle italic on a partial blank selection
|
||||
builder = new SpannableStringBuilder(" ");
|
||||
assertEquals(3, MarkdownUtil.togglePunctuation(builder, 1, 2, "*"));
|
||||
assertEquals(" * * ", builder.toString());
|
||||
|
||||
// Toggle bold on an empty text
|
||||
builder = new SpannableStringBuilder("");
|
||||
assertEquals(2, MarkdownUtil.togglePunctuation(builder, 0, 0, "**"));
|
||||
assertEquals("****", builder.toString());
|
||||
|
||||
// Toggle bold on a blank selection
|
||||
builder = new SpannableStringBuilder(" ");
|
||||
assertEquals(3, MarkdownUtil.togglePunctuation(builder, 0, 1, "**"));
|
||||
assertEquals("** **", builder.toString());
|
||||
|
||||
// Toggle bold on a partial blank selection
|
||||
builder = new SpannableStringBuilder(" ");
|
||||
assertEquals(4, MarkdownUtil.togglePunctuation(builder, 1, 2, "**"));
|
||||
assertEquals(" ** ** ", builder.toString());
|
||||
|
||||
// Toggle italic right after bold
|
||||
builder = new SpannableStringBuilder("**Bold**Italic");
|
||||
assertEquals(15, MarkdownUtil.togglePunctuation(builder, 8, 14, "*"));
|
||||
assertEquals("**Bold***Italic*", builder.toString());
|
||||
|
||||
// Toggle italic for last of many bolds in one line
|
||||
builder = new SpannableStringBuilder("Lorem **Ipsum** **Dolor**");
|
||||
assertEquals(24, MarkdownUtil.togglePunctuation(builder, 18, 23, "*"));
|
||||
assertEquals("Lorem **Ipsum** ***Dolor***", builder.toString());
|
||||
|
||||
builder = new SpannableStringBuilder("Lorem **Ipsum** **Dolor**");
|
||||
assertEquals(14, MarkdownUtil.togglePunctuation(builder, 8, 13, "*"));
|
||||
assertEquals("Lorem ***Ipsum*** **Dolor**", builder.toString());
|
||||
|
||||
builder = new SpannableStringBuilder("Lorem **Ipsum** **Dolor**");
|
||||
assertEquals(16, MarkdownUtil.togglePunctuation(builder, 6, 15, "*"));
|
||||
assertEquals("Lorem ***Ipsum*** **Dolor**", builder.toString());
|
||||
|
||||
// Toggle italic for last bold + italic in a row of multiple marked elements
|
||||
builder = new SpannableStringBuilder("Lorem **Ipsum** ***Dolor***");
|
||||
assertEquals(23, MarkdownUtil.togglePunctuation(builder, 19, 24, "*"));
|
||||
assertEquals("Lorem **Ipsum** **Dolor**", builder.toString());
|
||||
|
||||
builder = new SpannableStringBuilder("Lorem ***Ipsum*** **Dolor**");
|
||||
assertEquals(13, MarkdownUtil.togglePunctuation(builder, 9, 14, "*"));
|
||||
assertEquals("Lorem **Ipsum** **Dolor**", builder.toString());
|
||||
|
||||
builder = new SpannableStringBuilder("Lorem ***Ipsum*** **Dolor**");
|
||||
assertEquals(15, MarkdownUtil.togglePunctuation(builder, 6, 17, "*"));
|
||||
assertEquals("Lorem **Ipsum** **Dolor**", builder.toString());
|
||||
|
||||
builder = new SpannableStringBuilder("Lorem ***Ipsum*** **Dolor**");
|
||||
assertEquals(15, MarkdownUtil.togglePunctuation(builder, 7, 16, "*"));
|
||||
assertEquals("Lorem **Ipsum** **Dolor**", builder.toString());
|
||||
|
||||
builder = new SpannableStringBuilder("Lorem ***Ipsum*** **Dolor**");
|
||||
assertEquals(15, MarkdownUtil.togglePunctuation(builder, 7, 17, "*"));
|
||||
assertEquals("Lorem **Ipsum** **Dolor**", builder.toString());
|
||||
|
||||
builder = new SpannableStringBuilder("Lorem ***Ipsum*** **Dolor**");
|
||||
assertEquals(15, MarkdownUtil.togglePunctuation(builder, 8, 16, "*"));
|
||||
assertEquals("Lorem **Ipsum** **Dolor**", builder.toString());
|
||||
|
||||
// Multiline
|
||||
|
||||
builder = new SpannableStringBuilder("Lorem ***Ipsum***\n **Dolor**");
|
||||
assertEquals(29, MarkdownUtil.togglePunctuation(builder, 0, 28, "*"));
|
||||
assertEquals("*Lorem ***Ipsum***\n **Dolor***", builder.toString());
|
||||
|
||||
builder = new SpannableStringBuilder("**Bold**\nItalic");
|
||||
assertEquals(16, MarkdownUtil.togglePunctuation(builder, 9, 15, "*"));
|
||||
assertEquals("**Bold**\n*Italic*", builder.toString());
|
||||
|
||||
builder = new SpannableStringBuilder("Bold\n*Italic*");
|
||||
assertEquals(6, MarkdownUtil.togglePunctuation(builder, 0, 4, "**"));
|
||||
assertEquals("**Bold**\n*Italic*", builder.toString());
|
||||
|
||||
builder = new SpannableStringBuilder("*Italic*\nBold");
|
||||
assertEquals(15, MarkdownUtil.togglePunctuation(builder, 9, 13, "**"));
|
||||
assertEquals("*Italic*\n**Bold**", builder.toString());
|
||||
|
||||
builder = new SpannableStringBuilder("Italic\n**Bold**");
|
||||
assertEquals(7, MarkdownUtil.togglePunctuation(builder, 0, 6, "*"));
|
||||
assertEquals("*Italic*\n**Bold**", builder.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInsertLink() {
|
||||
Editable builder;
|
||||
|
||||
// Add link without clipboardUrl to normal text
|
||||
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
|
||||
assertEquals(14, MarkdownUtil.insertLink(builder, 6, 11, null));
|
||||
assertEquals("Lorem [ipsum]() dolor sit amet.", builder.toString());
|
||||
|
||||
// Add link without clipboardUrl to url
|
||||
builder = new SpannableStringBuilder("Lorem https://example.com dolor sit amet.");
|
||||
assertEquals(7, MarkdownUtil.insertLink(builder, 6, 25, null));
|
||||
assertEquals("Lorem [](https://example.com) dolor sit amet.", builder.toString());
|
||||
|
||||
// Add link without clipboardUrl to empty selection before space character
|
||||
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
|
||||
assertEquals(13, MarkdownUtil.insertLink(builder, 11, 11, null));
|
||||
assertEquals("Lorem ipsum []() dolor sit amet.", builder.toString());
|
||||
|
||||
// Add link without clipboardUrl to empty selection after space character
|
||||
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
|
||||
assertEquals(13, MarkdownUtil.insertLink(builder, 12, 12, null));
|
||||
assertEquals("Lorem ipsum []() dolor sit amet.", builder.toString());
|
||||
|
||||
// Add link without clipboardUrl to empty selection in word
|
||||
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
|
||||
assertEquals(20, MarkdownUtil.insertLink(builder, 14, 14, null));
|
||||
assertEquals("Lorem ipsum [dolor]() sit amet.", builder.toString());
|
||||
|
||||
// Add link with clipboardUrl to normal text
|
||||
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
|
||||
assertEquals(33, MarkdownUtil.insertLink(builder, 6, 11, "https://example.com"));
|
||||
assertEquals("Lorem [ipsum](https://example.com) dolor sit amet.", builder.toString());
|
||||
|
||||
// Add link with clipboardUrl to url
|
||||
builder = new SpannableStringBuilder("Lorem https://example.com dolor sit amet.");
|
||||
assertEquals(46, MarkdownUtil.insertLink(builder, 6, 25, "https://example.de"));
|
||||
assertEquals("Lorem [https://example.com](https://example.de) dolor sit amet.", builder.toString());
|
||||
|
||||
// Add link with clipboardUrl to empty selection before space character
|
||||
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
|
||||
assertEquals(13, MarkdownUtil.insertLink(builder, 11, 11, "https://example.de"));
|
||||
assertEquals("Lorem ipsum [](https://example.de) dolor sit amet.", builder.toString());
|
||||
|
||||
// Add link with clipboardUrl to empty selection after space character
|
||||
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
|
||||
assertEquals(13, MarkdownUtil.insertLink(builder, 12, 12, "https://example.de"));
|
||||
assertEquals("Lorem ipsum [](https://example.de) dolor sit amet.", builder.toString());
|
||||
|
||||
// Add link with clipboardUrl to empty selection in word
|
||||
builder = new SpannableStringBuilder("Lorem ipsum dolor sit amet.");
|
||||
assertEquals(38, MarkdownUtil.insertLink(builder, 14, 14, "https://example.de"));
|
||||
assertEquals("Lorem ipsum [dolor](https://example.de) sit amet.", builder.toString());
|
||||
|
||||
// Add link without clipboardUrl to empty selection on empty text
|
||||
builder = new SpannableStringBuilder("");
|
||||
assertEquals(1, MarkdownUtil.insertLink(builder, 0, 0, null));
|
||||
assertEquals("[]()", builder.toString());
|
||||
|
||||
// Add link without clipboardUrl to empty selection on only space text
|
||||
builder = new SpannableStringBuilder(" ");
|
||||
assertEquals(1, MarkdownUtil.insertLink(builder, 0, 0, null));
|
||||
assertEquals("[]() ", builder.toString());
|
||||
|
||||
// Add link without clipboardUrl to empty selection on only space text
|
||||
builder = new SpannableStringBuilder(" ");
|
||||
assertEquals(4, MarkdownUtil.insertLink(builder, 0, 1, null));
|
||||
assertEquals("[ ]()", builder.toString());
|
||||
|
||||
// Add link without clipboardUrl to empty selection on only space text
|
||||
builder = new SpannableStringBuilder(" ");
|
||||
assertEquals(2, MarkdownUtil.insertLink(builder, 1, 1, null));
|
||||
assertEquals(" []()", builder.toString());
|
||||
|
||||
// Add link without clipboardUrl to empty selection on only spaces
|
||||
builder = new SpannableStringBuilder(" ");
|
||||
assertEquals(2, MarkdownUtil.insertLink(builder, 1, 1, null));
|
||||
assertEquals(" []() ", builder.toString());
|
||||
|
||||
// Add link without clipboardUrl to empty selection in word with trailing and leading spaces
|
||||
builder = new SpannableStringBuilder(" Lorem ");
|
||||
assertEquals(10, MarkdownUtil.insertLink(builder, 5, 5, null));
|
||||
assertEquals(" [Lorem]() ", builder.toString());
|
||||
|
||||
// Add link with clipboardUrl to empty selection on empty text
|
||||
builder = new SpannableStringBuilder("");
|
||||
assertEquals(1, MarkdownUtil.insertLink(builder, 0, 0, "https://www.example.com"));
|
||||
assertEquals("[](https://www.example.com)", builder.toString());
|
||||
|
||||
// Add link with clipboardUrl to empty selection on only space text
|
||||
builder = new SpannableStringBuilder(" ");
|
||||
assertEquals(1, MarkdownUtil.insertLink(builder, 0, 0, "https://www.example.com"));
|
||||
assertEquals("[](https://www.example.com) ", builder.toString());
|
||||
|
||||
// Add link with clipboardUrl to empty selection on only space text
|
||||
builder = new SpannableStringBuilder(" ");
|
||||
assertEquals(27, MarkdownUtil.insertLink(builder, 0, 1, "https://www.example.com"));
|
||||
assertEquals("[ ](https://www.example.com)", builder.toString());
|
||||
|
||||
// Add link with clipboardUrl to empty selection on only space text
|
||||
builder = new SpannableStringBuilder(" ");
|
||||
assertEquals(2, MarkdownUtil.insertLink(builder, 1, 1, "https://www.example.com"));
|
||||
assertEquals(" [](https://www.example.com)", builder.toString());
|
||||
|
||||
// Add link with clipboardUrl to empty selection on one character
|
||||
builder = new SpannableStringBuilder("a");
|
||||
assertEquals(1, MarkdownUtil.insertLink(builder, 0, 0, "https://www.example.com"));
|
||||
assertEquals("[](https://www.example.com)a", builder.toString());
|
||||
|
||||
// Add link with clipboardUrl to empty selection on one character
|
||||
builder = new SpannableStringBuilder("a");
|
||||
assertEquals(27, MarkdownUtil.insertLink(builder, 0, 1, "https://www.example.com"));
|
||||
assertEquals("[a](https://www.example.com)", builder.toString());
|
||||
|
||||
// Add link with clipboardUrl to empty selection on one character
|
||||
builder = new SpannableStringBuilder("a");
|
||||
assertEquals(2, MarkdownUtil.insertLink(builder, 1, 1, "https://www.example.com"));
|
||||
assertEquals("a[](https://www.example.com)", builder.toString());
|
||||
|
||||
// Add link with clipboardUrl to empty selection on only spaces
|
||||
builder = new SpannableStringBuilder(" ");
|
||||
assertEquals(2, MarkdownUtil.insertLink(builder, 1, 1, "https://www.example.com"));
|
||||
assertEquals(" [](https://www.example.com) ", builder.toString());
|
||||
|
||||
// Add link with clipboardUrl to empty selection in word with trailing and leading spaces
|
||||
builder = new SpannableStringBuilder(" Lorem ");
|
||||
assertEquals(33, MarkdownUtil.insertLink(builder, 5, 5, "https://www.example.com"));
|
||||
assertEquals(" [Lorem](https://www.example.com) ", builder.toString());
|
||||
|
||||
// Add link with clipboardUrl to selection in word with trailing and leading spaces
|
||||
builder = new SpannableStringBuilder(" Lorem ");
|
||||
assertEquals(33, MarkdownUtil.insertLink(builder, 2, 7, "https://www.example.com"));
|
||||
assertEquals(" [Lorem](https://www.example.com) ", builder.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
public void testSelectionIsInLink() {
|
||||
try {
|
||||
final var method = MarkdownUtil.class.getDeclaredMethod("selectionIsInLink", CharSequence.class, int.class, int.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
assertTrue((Boolean) method.invoke(null, "Lorem [ipsum](https://example.com) dolor sit amet.", 7, 12));
|
||||
assertTrue((Boolean) method.invoke(null, "Lorem [ipsum](https://example.com) dolor sit amet.", 6, 34));
|
||||
assertTrue((Boolean) method.invoke(null, "Lorem [ipsum](https://example.com) dolor sit amet.", 14, 33));
|
||||
assertTrue((Boolean) method.invoke(null, "Lorem [ipsum](https://example.com) dolor sit amet.", 12, 14));
|
||||
assertTrue((Boolean) method.invoke(null, "Lorem [ipsum](https://example.com) dolor sit amet.", 0, 7));
|
||||
assertTrue((Boolean) method.invoke(null, "Lorem [ipsum](https://example.com) dolor sit amet.", 33, 34));
|
||||
|
||||
assertTrue((Boolean) method.invoke(null, "Lorem [](https://example.com) dolor sit amet.", 6, 28));
|
||||
assertTrue((Boolean) method.invoke(null, "Lorem [](https://example.com) dolor sit amet.", 7, 28));
|
||||
assertTrue((Boolean) method.invoke(null, "Lorem [](https://example.com) dolor sit amet.", 8, 28));
|
||||
assertTrue((Boolean) method.invoke(null, "Lorem [](https://example.com) dolor sit amet.", 9, 28));
|
||||
assertTrue((Boolean) method.invoke(null, "Lorem [](https://example.com) dolor sit amet.", 6, 29));
|
||||
assertTrue((Boolean) method.invoke(null, "Lorem [](https://example.com) dolor sit amet.", 7, 29));
|
||||
assertTrue((Boolean) method.invoke(null, "Lorem [](https://example.com) dolor sit amet.", 8, 29));
|
||||
assertTrue((Boolean) method.invoke(null, "Lorem [](https://example.com) dolor sit amet.", 9, 29));
|
||||
|
||||
assertTrue((Boolean) method.invoke(null, "Lorem [ipsum]() dolor sit amet.", 6, 12));
|
||||
assertTrue((Boolean) method.invoke(null, "Lorem [ipsum]() dolor sit amet.", 6, 13));
|
||||
assertTrue((Boolean) method.invoke(null, "Lorem [ipsum]() dolor sit amet.", 6, 14));
|
||||
assertTrue((Boolean) method.invoke(null, "Lorem [ipsum]() dolor sit amet.", 6, 15));
|
||||
assertTrue((Boolean) method.invoke(null, "Lorem [ipsum]() dolor sit amet.", 7, 12));
|
||||
assertTrue((Boolean) method.invoke(null, "Lorem [ipsum]() dolor sit amet.", 7, 13));
|
||||
assertTrue((Boolean) method.invoke(null, "Lorem [ipsum]() dolor sit amet.", 7, 14));
|
||||
assertTrue((Boolean) method.invoke(null, "Lorem [ipsum]() dolor sit amet.", 7, 15));
|
||||
|
||||
assertFalse((Boolean) method.invoke(null, "Lorem [ipsum](https://example.com) dolor sit amet.", 0, 6));
|
||||
assertFalse((Boolean) method.invoke(null, "Lorem [ipsum](https://example.com) dolor sit amet.", 34, 50));
|
||||
assertFalse((Boolean) method.invoke(null, "Lorem [ipsum](https://example.com) dolor sit amet.", 41, 44));
|
||||
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("OptionalGetWithoutIsPresent")
|
||||
@Test
|
||||
public void testGetListItemIfIsEmpty() {
|
||||
assertEquals("- ", MarkdownUtil.getListItemIfIsEmpty("- ").get());
|
||||
assertEquals("+ ", MarkdownUtil.getListItemIfIsEmpty("+ ").get());
|
||||
assertEquals("* ", MarkdownUtil.getListItemIfIsEmpty("* ").get());
|
||||
assertEquals("1. ", MarkdownUtil.getListItemIfIsEmpty("1. ").get());
|
||||
assertEquals(" - ", MarkdownUtil.getListItemIfIsEmpty(" - ").get());
|
||||
assertEquals(" + ", MarkdownUtil.getListItemIfIsEmpty(" + ").get());
|
||||
assertEquals(" * ", MarkdownUtil.getListItemIfIsEmpty(" * ").get());
|
||||
assertEquals(" 1. ", MarkdownUtil.getListItemIfIsEmpty(" 1. ").get());
|
||||
assertEquals(" - ", MarkdownUtil.getListItemIfIsEmpty(" - ").get());
|
||||
assertEquals(" + ", MarkdownUtil.getListItemIfIsEmpty(" + ").get());
|
||||
assertEquals(" * ", MarkdownUtil.getListItemIfIsEmpty(" * ").get());
|
||||
assertEquals(" 1. ", MarkdownUtil.getListItemIfIsEmpty(" 1. ").get());
|
||||
|
||||
assertFalse(MarkdownUtil.getListItemIfIsEmpty("- Test").isPresent());
|
||||
assertFalse(MarkdownUtil.getListItemIfIsEmpty("+ Test").isPresent());
|
||||
assertFalse(MarkdownUtil.getListItemIfIsEmpty("* Test").isPresent());
|
||||
assertFalse(MarkdownUtil.getListItemIfIsEmpty("1. s").isPresent());
|
||||
assertFalse(MarkdownUtil.getListItemIfIsEmpty("1. ").isPresent());
|
||||
assertFalse(MarkdownUtil.getListItemIfIsEmpty(" - Test").isPresent());
|
||||
assertFalse(MarkdownUtil.getListItemIfIsEmpty(" + Test").isPresent());
|
||||
assertFalse(MarkdownUtil.getListItemIfIsEmpty(" * Test").isPresent());
|
||||
assertFalse(MarkdownUtil.getListItemIfIsEmpty(" 1. s").isPresent());
|
||||
assertFalse(MarkdownUtil.getListItemIfIsEmpty(" 1. ").isPresent());
|
||||
assertFalse(MarkdownUtil.getListItemIfIsEmpty(" - Test").isPresent());
|
||||
assertFalse(MarkdownUtil.getListItemIfIsEmpty(" + Test").isPresent());
|
||||
assertFalse(MarkdownUtil.getListItemIfIsEmpty(" * Test").isPresent());
|
||||
assertFalse(MarkdownUtil.getListItemIfIsEmpty(" 1. s").isPresent());
|
||||
assertFalse(MarkdownUtil.getListItemIfIsEmpty(" 1. ").isPresent());
|
||||
}
|
||||
|
||||
@SuppressWarnings("OptionalGetWithoutIsPresent")
|
||||
@Test
|
||||
public void testLineStartsWithOrderedList() {
|
||||
assertEquals(1, MarkdownUtil.getOrderedListNumber("1. Test").get().intValue());
|
||||
assertEquals(2, MarkdownUtil.getOrderedListNumber("2. Test").get().intValue());
|
||||
assertEquals(3, MarkdownUtil.getOrderedListNumber("3. Test").get().intValue());
|
||||
assertEquals(10, MarkdownUtil.getOrderedListNumber("10. Test").get().intValue());
|
||||
assertEquals(11, MarkdownUtil.getOrderedListNumber("11. Test").get().intValue());
|
||||
assertEquals(12, MarkdownUtil.getOrderedListNumber("12. Test").get().intValue());
|
||||
assertEquals(1, MarkdownUtil.getOrderedListNumber("1. 1").get().intValue());
|
||||
assertEquals(1, MarkdownUtil.getOrderedListNumber("1. Test 1").get().intValue());
|
||||
|
||||
assertFalse(MarkdownUtil.getOrderedListNumber("").isPresent());
|
||||
assertFalse(MarkdownUtil.getOrderedListNumber("1.").isPresent());
|
||||
assertFalse(MarkdownUtil.getOrderedListNumber("1. ").isPresent());
|
||||
assertFalse(MarkdownUtil.getOrderedListNumber("11. ").isPresent());
|
||||
assertFalse(MarkdownUtil.getOrderedListNumber("-1. Test").isPresent());
|
||||
assertFalse(MarkdownUtil.getOrderedListNumber(" 1. Test").isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetCheckboxStatus() {
|
||||
for (final var listType : EListType.values()) {
|
||||
final String origin_1 = listType.checkboxUnchecked + " Item";
|
||||
final String expected_1 = listType.checkboxChecked + " Item";
|
||||
assertEquals(expected_1, MarkdownUtil.setCheckboxStatus(origin_1, 0, true));
|
||||
|
||||
final String origin_2 = listType.checkboxChecked + " Item";
|
||||
final String expected_2 = listType.checkboxChecked + " Item";
|
||||
assertEquals(expected_2, MarkdownUtil.setCheckboxStatus(origin_2, 0, true));
|
||||
|
||||
final String origin_3 = listType.checkboxChecked + " Item";
|
||||
final String expected_3 = listType.checkboxChecked + " Item";
|
||||
assertEquals(expected_3, MarkdownUtil.setCheckboxStatus(origin_3, -1, true));
|
||||
|
||||
final String origin_4 = listType.checkboxChecked + " Item";
|
||||
final String expected_4 = listType.checkboxChecked + " Item";
|
||||
assertEquals(expected_4, MarkdownUtil.setCheckboxStatus(origin_4, 3, true));
|
||||
|
||||
final String origin_5 = "" +
|
||||
listType.checkboxChecked + " Item\n" +
|
||||
listType.checkboxChecked + " Item";
|
||||
final String expected_5 = "" +
|
||||
listType.checkboxChecked + " Item\n" +
|
||||
listType.checkboxUnchecked + " Item";
|
||||
assertEquals(expected_5, MarkdownUtil.setCheckboxStatus(origin_5, 1, false));
|
||||
|
||||
// Checkboxes in fenced code block aren't rendered by Markwon and therefore don't count to the checkbox index
|
||||
final String origin_6 = "" +
|
||||
listType.checkboxChecked + " Item\n" +
|
||||
"```\n" +
|
||||
listType.checkboxUnchecked + " Item\n" +
|
||||
"```\n" +
|
||||
listType.checkboxUnchecked + " Item";
|
||||
final String expected_6 = "" +
|
||||
listType.checkboxChecked + " Item\n" +
|
||||
"```\n" +
|
||||
listType.checkboxUnchecked + " Item\n" +
|
||||
"```\n" +
|
||||
listType.checkboxChecked + " Item";
|
||||
assertEquals(expected_6, MarkdownUtil.setCheckboxStatus(origin_6, 1, true));
|
||||
|
||||
// Checkbox in partial nested fenced code block does not count as rendered checkbox
|
||||
final String origin_7 = "" +
|
||||
listType.checkboxChecked + " Item\n" +
|
||||
"````\n" +
|
||||
"```\n" +
|
||||
listType.checkboxUnchecked + " Item\n" +
|
||||
"````\n" +
|
||||
listType.checkboxUnchecked + " Item";
|
||||
final String expected_7 = "" +
|
||||
listType.checkboxChecked + " Item\n" +
|
||||
"````\n" +
|
||||
"```\n" +
|
||||
listType.checkboxUnchecked + " Item\n" +
|
||||
"````\n" +
|
||||
listType.checkboxChecked + " Item";
|
||||
assertEquals(expected_7, MarkdownUtil.setCheckboxStatus(origin_7, 1, true));
|
||||
|
||||
// Checkbox in complete nested fenced code block does not count as rendered checkbox
|
||||
final String origin_8 = "" +
|
||||
listType.checkboxChecked + " Item\n" +
|
||||
"````\n" +
|
||||
"```\n" +
|
||||
listType.checkboxUnchecked + " Item\n" +
|
||||
"```\n" +
|
||||
"````\n" +
|
||||
listType.checkboxUnchecked + " Item";
|
||||
final String expected_8 = "" +
|
||||
listType.checkboxChecked + " Item\n" +
|
||||
"````\n" +
|
||||
"```\n" +
|
||||
listType.checkboxUnchecked + " Item\n" +
|
||||
"```\n" +
|
||||
"````\n" +
|
||||
listType.checkboxChecked + " Item";
|
||||
assertEquals(expected_8, MarkdownUtil.setCheckboxStatus(origin_8, 1, true));
|
||||
|
||||
// If checkbox has no content, it doesn't get rendered by Markwon and therefore can not be checked
|
||||
final String origin_9 = "" +
|
||||
listType.checkboxChecked + " Item\n" +
|
||||
"````\n" +
|
||||
"```\n" +
|
||||
listType.checkboxUnchecked + " Item\n" +
|
||||
"```\n" +
|
||||
"````\n" +
|
||||
listType.checkboxUnchecked + " \n" +
|
||||
listType.checkboxUnchecked + " Item";
|
||||
final String expected_9 = "" +
|
||||
listType.checkboxChecked + " Item\n" +
|
||||
"````\n" +
|
||||
"```\n" +
|
||||
listType.checkboxUnchecked + " Item\n" +
|
||||
"```\n" +
|
||||
"````\n" +
|
||||
listType.checkboxUnchecked + " \n" +
|
||||
listType.checkboxChecked + " Item";
|
||||
assertEquals(expected_9, MarkdownUtil.setCheckboxStatus(origin_9, 1, true));
|
||||
|
||||
final String origin_10 = "" +
|
||||
listType.checkboxChecked + " Item\n" +
|
||||
listType.checkboxCheckedUpperCase + " Item";
|
||||
final String expected_10 = "" +
|
||||
listType.checkboxChecked + " Item\n" +
|
||||
listType.checkboxUnchecked + " Item";
|
||||
assertEquals(expected_10, MarkdownUtil.setCheckboxStatus(origin_10, 1, false));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveSpans() {
|
||||
try {
|
||||
final var removeSpans = MarkdownUtil.class.getDeclaredMethod("removeSpans", Spannable.class, Class.class);
|
||||
removeSpans.setAccessible(true);
|
||||
|
||||
final var editable_1 = new SpannableStringBuilder("Lorem Ipsum dolor sit amet");
|
||||
editable_1.setSpan(new SearchSpan(Color.RED, Color.GRAY, false, false), 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
editable_1.setSpan(new ForegroundColorSpan(Color.BLUE), 6, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
editable_1.setSpan(new SearchSpan(Color.BLUE, Color.GREEN, true, false), 12, 17, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
removeSpans.invoke(null, editable_1, SearchSpan.class);
|
||||
assertEquals(0, editable_1.getSpans(0, editable_1.length(), SearchSpan.class).length);
|
||||
assertEquals(1, editable_1.getSpans(0, editable_1.length(), ForegroundColorSpan.class).length);
|
||||
|
||||
final var editable_2 = new SpannableStringBuilder("Lorem Ipsum dolor sit amet");
|
||||
editable_2.setSpan(new SearchSpan(Color.GRAY, Color.RED, false, true), 0, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
editable_2.setSpan(new ForegroundColorSpan(Color.BLUE), 2, 7, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
editable_2.setSpan(new SearchSpan(Color.BLUE, Color.GREEN, true, false), 3, 9, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
removeSpans.invoke(null, editable_2, SearchSpan.class);
|
||||
assertEquals(0, editable_2.getSpans(0, editable_2.length(), SearchSpan.class).length);
|
||||
assertEquals(1, editable_2.getSpans(0, editable_2.length(), ForegroundColorSpan.class).length);
|
||||
assertEquals(2, editable_2.getSpanStart(editable_2.getSpans(0, editable_2.length(), ForegroundColorSpan.class)[0]));
|
||||
assertEquals(7, editable_2.getSpanEnd(editable_2.getSpans(0, editable_2.length(), ForegroundColorSpan.class)[0]));
|
||||
|
||||
final var editable_3 = new SpannableStringBuilder("Lorem Ipsum dolor sit amet");
|
||||
editable_3.setSpan(new ForegroundColorSpan(Color.BLUE), 2, 7, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
removeSpans.invoke(null, editable_3, SearchSpan.class);
|
||||
assertEquals(0, editable_3.getSpans(0, editable_3.length(), SearchSpan.class).length);
|
||||
assertEquals(1, editable_3.getSpans(0, editable_3.length(), ForegroundColorSpan.class).length);
|
||||
assertEquals(2, editable_3.getSpanStart(editable_3.getSpans(0, editable_3.length(), ForegroundColorSpan.class)[0]));
|
||||
assertEquals(7, editable_3.getSpanEnd(editable_3.getSpans(0, editable_3.length(), ForegroundColorSpan.class)[0]));
|
||||
|
||||
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveMarkdown() {
|
||||
assertEquals("Test", MarkdownUtil.removeMarkdown("Test"));
|
||||
assertEquals("Foo\nBar", MarkdownUtil.removeMarkdown("Foo\nBar"));
|
||||
assertEquals("Foo\nBar", MarkdownUtil.removeMarkdown("Foo\n Bar"));
|
||||
assertEquals("Foo\nBar", MarkdownUtil.removeMarkdown("Foo \nBar"));
|
||||
assertEquals("Foo-Bar", MarkdownUtil.removeMarkdown("Foo-Bar"));
|
||||
assertEquals("Foo*Bar", MarkdownUtil.removeMarkdown("Foo*Bar"));
|
||||
assertEquals("Foo/Bar", MarkdownUtil.removeMarkdown("Foo/Bar"));
|
||||
assertEquals("FooTestBar", MarkdownUtil.removeMarkdown("Foo*Test*Bar"));
|
||||
assertEquals("FooTestBar", MarkdownUtil.removeMarkdown("Foo**Test**Bar"));
|
||||
assertEquals("FooTestBar", MarkdownUtil.removeMarkdown("Foo***Test***Bar"));
|
||||
assertEquals("Foo*Test**Bar", MarkdownUtil.removeMarkdown("Foo*Test**Bar"));
|
||||
assertEquals("Foo*TestBar", MarkdownUtil.removeMarkdown("Foo***Test**Bar"));
|
||||
assertEquals("Foo_Test_Bar", MarkdownUtil.removeMarkdown("Foo_Test_Bar"));
|
||||
assertEquals("Foo__Test__Bar", MarkdownUtil.removeMarkdown("Foo__Test__Bar"));
|
||||
assertEquals("Foo___Test___Bar", MarkdownUtil.removeMarkdown("Foo___Test___Bar"));
|
||||
assertEquals("Foo\nHeader\nBar", MarkdownUtil.removeMarkdown("Foo\n# Header\nBar"));
|
||||
assertEquals("Foo\nHeader\nBar", MarkdownUtil.removeMarkdown("Foo\n### Header\nBar"));
|
||||
assertEquals("Foo\nHeader\nBar", MarkdownUtil.removeMarkdown("Foo\n# Header #\nBar"));
|
||||
assertEquals("Foo\nHeader\nBar", MarkdownUtil.removeMarkdown("Foo\n## Header ####\nBar"));
|
||||
assertEquals("Foo\nNo Header #\nBar", MarkdownUtil.removeMarkdown("Foo\nNo Header #\nBar"));
|
||||
assertEquals("Foo\nHeader\nBar", MarkdownUtil.removeMarkdown("Foo\nHeader\n=\nBar"));
|
||||
assertEquals("Foo\nHeader\nBar", MarkdownUtil.removeMarkdown("Foo\nHeader\n-----\nBar"));
|
||||
assertEquals("Foo\nHeader\n--=--\nBar", MarkdownUtil.removeMarkdown("Foo\nHeader\n--=--\nBar"));
|
||||
assertEquals("Foo\nAufzählung\nBar", MarkdownUtil.removeMarkdown("Foo\n* Aufzählung\nBar"));
|
||||
assertEquals("Foo\nAufzählung\nBar", MarkdownUtil.removeMarkdown("Foo\n+ Aufzählung\nBar"));
|
||||
assertEquals("Foo\nAufzählung\nBar", MarkdownUtil.removeMarkdown("Foo\n- Aufzählung\nBar"));
|
||||
assertEquals("Foo\n- Aufzählung\nBar", MarkdownUtil.removeMarkdown("Foo\n - Aufzählung\nBar"));
|
||||
assertEquals("Foo\nAufzählung *\nBar", MarkdownUtil.removeMarkdown("Foo\n* Aufzählung *\nBar"));
|
||||
assertEquals("Title", MarkdownUtil.removeMarkdown("# Title"));
|
||||
assertEquals("Aufzählung", MarkdownUtil.removeMarkdown("* Aufzählung"));
|
||||
// assertEquals("Foo Link Bar", MarkdownUtil.removeMarkdown("Foo [Link](https://example.com) Bar"));
|
||||
assertFalse(MarkdownUtil.removeMarkdown("- [ ] Test").contains("- [ ]"));
|
||||
assertTrue(MarkdownUtil.removeMarkdown("- [ ] Test").endsWith("Test"));
|
||||
assertEquals("", MarkdownUtil.removeMarkdown(null));
|
||||
assertEquals("", MarkdownUtil.removeMarkdown(""));
|
||||
|
||||
// https://github.com/stefan-niedermann/nextcloud-notes/issues/1104
|
||||
assertEquals("2021-03-24 - Example text", MarkdownUtil.removeMarkdown("2021-03-24 - Example text"));
|
||||
}
|
||||
}
|
|
@ -1,210 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.plugins;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.URLSpan;
|
||||
import android.util.Range;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.commonmark.node.Node;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.noties.markwon.MarkwonVisitor;
|
||||
import io.noties.markwon.SpannableBuilder;
|
||||
import io.noties.markwon.ext.tasklist.TaskListSpan;
|
||||
import it.niedermann.android.markdown.markwon.span.InterceptedURLSpan;
|
||||
import it.niedermann.android.markdown.markwon.span.ToggleTaskListSpan;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class ToggleableTaskListPluginTest extends TestCase {
|
||||
|
||||
@Test
|
||||
public void testAfterRender() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
|
||||
final var node = mock(Node.class);
|
||||
final var visitor = mock(MarkwonVisitor.class);
|
||||
|
||||
final var markerSpanConstructor = ToggleableTaskListPlugin.ToggleMarkerSpan.class.getDeclaredConstructor(TaskListSpan.class);
|
||||
markerSpanConstructor.setAccessible(true);
|
||||
|
||||
final var builder = new SpannableBuilder("Lorem Ipsum Dolor \nSit Amet");
|
||||
builder.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 0, 6);
|
||||
builder.setSpan(new URLSpan(""), 6, 11);
|
||||
builder.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 11, 19);
|
||||
builder.setSpan(new InterceptedURLSpan(Collections.emptyList(), ""), 19, 22);
|
||||
builder.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 22, 27);
|
||||
|
||||
when(visitor.builder()).thenReturn(builder);
|
||||
|
||||
final var plugin = new ToggleableTaskListPlugin((i, b) -> {
|
||||
// Do nothing...
|
||||
});
|
||||
plugin.afterRender(node, visitor);
|
||||
|
||||
// We ignore marker spans in this test. They will be removed in another step
|
||||
final var spans = builder.getSpans(0, builder.length())
|
||||
.stream()
|
||||
.filter(span -> span.what.getClass() != ToggleableTaskListPlugin.ToggleMarkerSpan.class)
|
||||
.sorted((o1, o2) -> o1.start - o2.start)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
assertEquals(5, spans.size());
|
||||
assertEquals(ToggleTaskListSpan.class, spans.get(0).what.getClass());
|
||||
assertEquals(0, spans.get(0).start);
|
||||
assertEquals(6, spans.get(0).end);
|
||||
assertEquals(URLSpan.class, spans.get(1).what.getClass());
|
||||
assertEquals(6, spans.get(1).start);
|
||||
assertEquals(11, spans.get(1).end);
|
||||
assertEquals(ToggleTaskListSpan.class, spans.get(2).what.getClass());
|
||||
assertEquals(11, spans.get(2).start);
|
||||
assertEquals(19, spans.get(2).end);
|
||||
assertEquals(InterceptedURLSpan.class, spans.get(3).what.getClass());
|
||||
assertEquals(19, spans.get(3).start);
|
||||
assertEquals(22, spans.get(3).end);
|
||||
assertEquals(ToggleTaskListSpan.class, spans.get(4).what.getClass());
|
||||
assertEquals(22, spans.get(4).start);
|
||||
assertEquals(27, spans.get(4).end);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAfterSetText() throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
|
||||
final var markerSpanConstructor = ToggleableTaskListPlugin.ToggleMarkerSpan.class.getDeclaredConstructor(TaskListSpan.class);
|
||||
markerSpanConstructor.setAccessible(true);
|
||||
|
||||
final var editable = new SpannableStringBuilder("Lorem Ipsum Dolor \nSit Amet");
|
||||
editable.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 0, 6, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
editable.setSpan(new URLSpan(""), 6, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
editable.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 11, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
editable.setSpan(new InterceptedURLSpan(Collections.emptyList(), ""), 19, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
editable.setSpan(markerSpanConstructor.newInstance(mock(TaskListSpan.class)), 22, 27, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
final var textView = new TextView(ApplicationProvider.getApplicationContext());
|
||||
textView.setText(editable);
|
||||
|
||||
assertEquals(3, ((Spanned) textView.getText()).getSpans(0, textView.getText().length(), ToggleableTaskListPlugin.ToggleMarkerSpan.class).length);
|
||||
|
||||
final var plugin = new ToggleableTaskListPlugin((i, b) -> {
|
||||
// Do nothing...
|
||||
});
|
||||
plugin.afterSetText(textView);
|
||||
|
||||
assertEquals(0, ((Spanned) textView.getText()).getSpans(0, textView.getText().length(), ToggleableTaskListPlugin.ToggleMarkerSpan.class).length);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"unchecked", "ConstantConditions"})
|
||||
public void testGetSortedSpans() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
|
||||
final var method = ToggleableTaskListPlugin.class.getDeclaredMethod("getSortedSpans", SpannableBuilder.class, Class.class, int.class, int.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
final var firstClickableSpan = new URLSpan("");
|
||||
final var secondClickableSpan = new InterceptedURLSpan(Collections.emptyList(), "");
|
||||
final var unclickableSpan = new ForegroundColorSpan(android.R.color.white);
|
||||
|
||||
final var spannable = new SpannableBuilder("Lorem Ipsum Dolor \nSit Amet");
|
||||
spannable.setSpan(firstClickableSpan, 6, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
spannable.setSpan(secondClickableSpan, 19, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
spannable.setSpan(unclickableSpan, 3, 20, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
List<SpannableBuilder.Span> clickableSpans;
|
||||
|
||||
clickableSpans = (List<SpannableBuilder.Span>) method.invoke(null, spannable, ClickableSpan.class, 0, 0);
|
||||
assertEquals(0, clickableSpans.size());
|
||||
|
||||
clickableSpans = (List<SpannableBuilder.Span>) method.invoke(null, spannable, ClickableSpan.class, spannable.length() - 1, spannable.length() - 1);
|
||||
assertEquals(0, clickableSpans.size());
|
||||
|
||||
clickableSpans = (List<SpannableBuilder.Span>) method.invoke(null, spannable, ClickableSpan.class, 0, 5);
|
||||
assertEquals(0, clickableSpans.size());
|
||||
|
||||
clickableSpans = (List<SpannableBuilder.Span>) method.invoke(null, spannable, ClickableSpan.class, 0, spannable.length());
|
||||
assertEquals(2, clickableSpans.size());
|
||||
assertEquals(firstClickableSpan, clickableSpans.get(0).what);
|
||||
assertEquals(secondClickableSpan, clickableSpans.get(1).what);
|
||||
|
||||
clickableSpans = (List<SpannableBuilder.Span>) method.invoke(null, spannable, ClickableSpan.class, 0, 17);
|
||||
assertEquals(1, clickableSpans.size());
|
||||
assertEquals(firstClickableSpan, clickableSpans.get(0).what);
|
||||
|
||||
clickableSpans = (List<SpannableBuilder.Span>) method.invoke(null, spannable, ClickableSpan.class, 12, 22);
|
||||
assertEquals(1, clickableSpans.size());
|
||||
assertEquals(secondClickableSpan, clickableSpans.get(0).what);
|
||||
|
||||
clickableSpans = (List<SpannableBuilder.Span>) method.invoke(null, spannable, ClickableSpan.class, 9, 20);
|
||||
assertEquals(2, clickableSpans.size());
|
||||
assertEquals(firstClickableSpan, clickableSpans.get(0).what);
|
||||
assertEquals(secondClickableSpan, clickableSpans.get(1).what);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"unchecked", "ConstantConditions"})
|
||||
public void testFindFreeRanges() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
|
||||
final var method = ToggleableTaskListPlugin.class.getDeclaredMethod("findFreeRanges", SpannableBuilder.class, int.class, int.class);
|
||||
method.setAccessible(true);
|
||||
|
||||
final var firstClickableSpan = new URLSpan("");
|
||||
final var secondClickableSpan = new InterceptedURLSpan(Collections.emptyList(), "");
|
||||
var spannable = new SpannableBuilder("Lorem Ipsum Dolor \nSit Amet");
|
||||
spannable.setSpan(firstClickableSpan, 6, 11, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
spannable.setSpan(secondClickableSpan, 19, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
List<Range<Integer>> freeRanges;
|
||||
|
||||
freeRanges = (List<Range<Integer>>) method.invoke(null, spannable, 0, 0);
|
||||
assertEquals(0, freeRanges.size());
|
||||
|
||||
freeRanges = (List<Range<Integer>>) method.invoke(null, spannable, spannable.length() - 1, spannable.length() - 1);
|
||||
assertEquals(0, freeRanges.size());
|
||||
|
||||
freeRanges = (List<Range<Integer>>) method.invoke(null, spannable, 0, 6);
|
||||
assertEquals(1, freeRanges.size());
|
||||
assertEquals(0, (int) freeRanges.get(0).getLower());
|
||||
assertEquals(6, (int) freeRanges.get(0).getUpper());
|
||||
|
||||
freeRanges = (List<Range<Integer>>) method.invoke(null, spannable, 0, 6);
|
||||
assertEquals(1, freeRanges.size());
|
||||
assertEquals(0, (int) freeRanges.get(0).getLower());
|
||||
assertEquals(6, (int) freeRanges.get(0).getUpper());
|
||||
|
||||
freeRanges = (List<Range<Integer>>) method.invoke(null, spannable, 3, 15);
|
||||
assertEquals(2, freeRanges.size());
|
||||
assertEquals(3, (int) freeRanges.get(0).getLower());
|
||||
assertEquals(6, (int) freeRanges.get(0).getUpper());
|
||||
assertEquals(11, (int) freeRanges.get(1).getLower());
|
||||
assertEquals(15, (int) freeRanges.get(1).getUpper());
|
||||
|
||||
freeRanges = (List<Range<Integer>>) method.invoke(null, spannable, 0, spannable.length());
|
||||
assertEquals(3, freeRanges.size());
|
||||
assertEquals(0, (int) freeRanges.get(0).getLower());
|
||||
assertEquals(6, (int) freeRanges.get(0).getUpper());
|
||||
assertEquals(11, (int) freeRanges.get(1).getLower());
|
||||
assertEquals(19, (int) freeRanges.get(1).getUpper());
|
||||
assertEquals(22, (int) freeRanges.get(2).getLower());
|
||||
assertEquals(27, (int) freeRanges.get(2).getUpper());
|
||||
|
||||
|
||||
// https://github.com/stefan-niedermann/nextcloud-notes/issues/1326
|
||||
|
||||
spannable = new SpannableBuilder("Test Crash\n\nJust text with link https://github.com");
|
||||
spannable.setSpan(firstClickableSpan, 32, 50, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
spannable.setSpan(secondClickableSpan, 32, 50, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
freeRanges = (List<Range<Integer>>) method.invoke(null, spannable, 12, 50);
|
||||
assertEquals(1, freeRanges.size());
|
||||
}
|
||||
}
|
|
@ -1,146 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.textwatcher;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
import it.niedermann.android.markdown.markwon.MarkwonMarkdownEditor;
|
||||
import it.niedermann.android.markdown.model.EListType;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class AutoContinuationTextWatcherTest extends TestCase {
|
||||
|
||||
@Rule
|
||||
public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
|
||||
|
||||
private EditText editText;
|
||||
|
||||
@Before
|
||||
public void reset() {
|
||||
this.editText = new MarkwonMarkdownEditor(ApplicationProvider.getApplicationContext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldContinueSimpleLists() {
|
||||
for (EListType listType : EListType.values()) {
|
||||
this.editText.setText(listType.listSymbolWithTrailingSpace + "Test");
|
||||
pressEnter(6);
|
||||
assertText(listType.listSymbolWithTrailingSpace + "Test\n" + listType.listSymbolWithTrailingSpace, 9);
|
||||
}
|
||||
for (EListType listType : EListType.values()) {
|
||||
this.editText.setText(listType.checkboxUncheckedWithTrailingSpace + "Test");
|
||||
pressEnter(10);
|
||||
assertText(listType.checkboxUncheckedWithTrailingSpace + "Test\n" + listType.checkboxUncheckedWithTrailingSpace, 17);
|
||||
}
|
||||
for (EListType listType : EListType.values()) {
|
||||
this.editText.setText(listType.checkboxChecked + " Test");
|
||||
pressEnter(10);
|
||||
assertText(listType.checkboxChecked + " Test\n" + listType.checkboxUncheckedWithTrailingSpace, 17);
|
||||
}
|
||||
|
||||
this.editText.setText("1. Test");
|
||||
pressEnter(7);
|
||||
assertText("1. Test\n2. ", 11);
|
||||
|
||||
this.editText.setText("11. Test");
|
||||
pressEnter(8);
|
||||
assertText("11. Test\n12. ", 13);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldContinueListsWithMultipleItems() {
|
||||
final CharSequence sample = "- [ ] Foo\n- [x] Bar\n- [ ] Baz\n\nQux";
|
||||
|
||||
this.editText.setText(sample);
|
||||
pressEnter(0);
|
||||
assertText("\n- [ ] Foo\n- [x] Bar\n- [ ] Baz\n\nQux", 1);
|
||||
|
||||
this.editText.setText(sample);
|
||||
pressEnter(9);
|
||||
assertText("- [ ] Foo\n- [ ] \n- [x] Bar\n- [ ] Baz\n\nQux", 16);
|
||||
|
||||
this.editText.setText(sample);
|
||||
pressEnter(19);
|
||||
assertText("- [ ] Foo\n- [x] Bar\n- [ ] \n- [ ] Baz\n\nQux", 26);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldSplitItemIfCursorIsNotOnEnd() {
|
||||
this.editText.setText("- [ ] Foo\n- [x] Bar");
|
||||
pressEnter(8);
|
||||
assertText("- [ ] Fo\n- [ ] o\n- [x] Bar", 15);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldContinueNestedListsAtTheSameIndention() {
|
||||
this.editText.setText("- [ ] Parent\n - [x] Child\n - [ ] Third");
|
||||
pressEnter(26);
|
||||
assertText("- [ ] Parent\n - [x] Child\n - [ ] \n - [ ] Third", 35);
|
||||
|
||||
this.editText.setText("- [ ] Parent\n - [x] Child\n - [ ] Third");
|
||||
pressEnter(42);
|
||||
assertText("- [ ] Parent\n - [x] Child\n - [ ] Third\n - [ ] ", 53);
|
||||
|
||||
this.editText.setText("1. Parent\n 1. Child");
|
||||
pressEnter(20);
|
||||
assertText("1. Parent\n 1. Child\n 2. ", 26);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldRemoveContinuedListItemWhenPressingEnterMultipleTimes() {
|
||||
this.editText.setText("- Foo");
|
||||
pressEnter(5);
|
||||
assertText("- Foo\n- ", 8);
|
||||
pressEnter(8);
|
||||
assertText("- Foo\n\n", 7);
|
||||
pressEnter(7);
|
||||
assertText("- Foo\n\n\n", 8);
|
||||
|
||||
this.editText.setText("- Foo\n - Bar");
|
||||
pressEnter(13);
|
||||
assertText("- Foo\n - Bar\n - ", 18);
|
||||
pressEnter(18);
|
||||
assertText("- Foo\n - Bar\n\n", 15);
|
||||
pressEnter(15);
|
||||
assertText("- Foo\n - Bar\n\n\n", 16);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotContinueIfNoList() {
|
||||
this.editText.setText("Foo");
|
||||
pressEnter(3);
|
||||
assertText("Foo\n", 4);
|
||||
|
||||
this.editText.setText(" Foo");
|
||||
pressEnter(4);
|
||||
assertText(" Foo\n", 5);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotContinueIfBlank() {
|
||||
this.editText.setText("");
|
||||
pressEnter(0);
|
||||
assertText("\n", 1);
|
||||
}
|
||||
|
||||
private void assertText(String expected, int cursorPosition) {
|
||||
assertEquals(expected, this.editText.getText().toString());
|
||||
assertEquals(cursorPosition, this.editText.getSelectionStart());
|
||||
assertEquals(cursorPosition, this.editText.getSelectionEnd());
|
||||
}
|
||||
|
||||
private void pressEnter(int atPosition) {
|
||||
this.editText.setSelection(atPosition);
|
||||
this.editText.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
|
||||
}
|
||||
}
|
|
@ -1,120 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.textwatcher;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
import it.niedermann.android.markdown.markwon.MarkwonMarkdownEditor;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class LowerIndentionTextWatcherTest extends TestCase {
|
||||
|
||||
@Rule
|
||||
public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
|
||||
|
||||
private EditText editText;
|
||||
|
||||
@Before
|
||||
public void reset() {
|
||||
this.editText = new MarkwonMarkdownEditor(ApplicationProvider.getApplicationContext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldLowerIndentionByTwoWhenPressingBackspaceOnAnIndentedList() {
|
||||
this.editText.setText(" - ");
|
||||
pressBackspace(4);
|
||||
assertText("- ", 2);
|
||||
|
||||
this.editText.setText("- [ ] Foo\n - [ ] ");
|
||||
pressBackspace(18);
|
||||
assertText("- [ ] Foo\n- [ ] ", 16);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldLowerIndentionByOneWhenPressingBackspaceOnAListWhichIsIndentedByOneSpace() {
|
||||
this.editText.setText(" - ");
|
||||
pressBackspace(3);
|
||||
assertText("- ", 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotLowerIndentionByOneWhenCursorIsNotAtTheEnd() {
|
||||
this.editText.setText(" - ");
|
||||
pressBackspace(2);
|
||||
assertText(" ", 1);
|
||||
|
||||
this.editText.setText(" - ");
|
||||
pressBackspace(0);
|
||||
assertText(" - ", 0);
|
||||
|
||||
this.editText.setText(" - ");
|
||||
pressBackspace(3);
|
||||
assertText(" ", 2);
|
||||
|
||||
this.editText.setText(" - ");
|
||||
pressBackspace(2);
|
||||
assertText(" - ", 1);
|
||||
|
||||
this.editText.setText("- Foo\n - ");
|
||||
pressBackspace(9);
|
||||
assertText("- Foo\n ", 8);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotLowerIndentionIfThereIsAnyContentAfterTheList() {
|
||||
this.editText.setText(" - ");
|
||||
pressBackspace(4);
|
||||
assertText(" - ", 3);
|
||||
|
||||
this.editText.setText(" - ");
|
||||
pressBackspace(5);
|
||||
assertText(" - ", 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotLowerIndentionIfBackspaceWasPressedInTheNextLine() {
|
||||
this.editText.setText(" - \nFoo");
|
||||
pressBackspace(5);
|
||||
assertText(" - Foo", 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldDeleteLastCharacterWhenPressingBackspace() {
|
||||
this.editText.setText("");
|
||||
pressBackspace(0);
|
||||
assertText("", 0);
|
||||
|
||||
this.editText.setText("- [ ] ");
|
||||
pressBackspace(6);
|
||||
assertText("- [ ]", 5);
|
||||
|
||||
this.editText.setText("- Foo");
|
||||
pressBackspace(5);
|
||||
assertText("- Fo", 4);
|
||||
|
||||
this.editText.setText("- [ ] Foo");
|
||||
pressBackspace(9);
|
||||
assertText("- [ ] Fo", 8);
|
||||
}
|
||||
|
||||
private void assertText(String expected, int cursorPosition) {
|
||||
assertEquals(expected, this.editText.getText().toString());
|
||||
assertEquals(cursorPosition, this.editText.getSelectionStart());
|
||||
assertEquals(cursorPosition, this.editText.getSelectionEnd());
|
||||
}
|
||||
|
||||
private void pressBackspace(int atPosition) {
|
||||
this.editText.setSelection(atPosition);
|
||||
this.editText.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
|
||||
}
|
||||
}
|
|
@ -1,167 +0,0 @@
|
|||
package it.niedermann.android.markdown.markwon.textwatcher;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule;
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
|
||||
import it.niedermann.android.markdown.markwon.MarkwonMarkdownEditor;
|
||||
import it.niedermann.android.markdown.model.EListType;
|
||||
|
||||
import static android.view.KeyEvent.KEYCODE_1;
|
||||
import static android.view.KeyEvent.KEYCODE_A;
|
||||
import static android.view.KeyEvent.KEYCODE_B;
|
||||
import static android.view.KeyEvent.KEYCODE_C;
|
||||
import static android.view.KeyEvent.KEYCODE_CTRL_LEFT;
|
||||
import static android.view.KeyEvent.KEYCODE_F;
|
||||
import static android.view.KeyEvent.KEYCODE_LEFT_BRACKET;
|
||||
import static android.view.KeyEvent.KEYCODE_MINUS;
|
||||
import static android.view.KeyEvent.KEYCODE_O;
|
||||
import static android.view.KeyEvent.KEYCODE_PERIOD;
|
||||
import static android.view.KeyEvent.KEYCODE_Q;
|
||||
import static android.view.KeyEvent.KEYCODE_R;
|
||||
import static android.view.KeyEvent.KEYCODE_RIGHT_BRACKET;
|
||||
import static android.view.KeyEvent.KEYCODE_SPACE;
|
||||
import static android.view.KeyEvent.KEYCODE_STAR;
|
||||
import static android.view.KeyEvent.KEYCODE_U;
|
||||
import static android.view.KeyEvent.KEYCODE_X;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
public class TextWatcherIntegrationTest extends TestCase {
|
||||
|
||||
@Rule
|
||||
public InstantTaskExecutorRule instantTaskExecutorRule = new InstantTaskExecutorRule();
|
||||
|
||||
private EditText editText;
|
||||
|
||||
@Before
|
||||
public void reset() {
|
||||
this.editText = new MarkwonMarkdownEditor(ApplicationProvider.getApplicationContext());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldSuccessfullyHandleMultipleTextEdits() {
|
||||
this.editText.setText("");
|
||||
assertEquals("");
|
||||
|
||||
sendKeys(KEYCODE_F, KEYCODE_O, KEYCODE_O);
|
||||
assertEquals("foo");
|
||||
|
||||
pressEnter();
|
||||
assertEquals("foo\n");
|
||||
|
||||
sendCheckboxKeys();
|
||||
assertEquals("foo\n- [ ] ");
|
||||
|
||||
pressEnter();
|
||||
assertEquals("foo\n\n");
|
||||
|
||||
sendCheckboxKeys();
|
||||
sendKeys(KEYCODE_B, KEYCODE_A, KEYCODE_R);
|
||||
assertEquals("foo\n\n- [ ] bar");
|
||||
|
||||
pressEnter();
|
||||
assertEquals("foo\n\n- [ ] bar\n- [ ] ");
|
||||
|
||||
pressEnter();
|
||||
assertEquals("foo\n\n- [ ] bar\n\n");
|
||||
|
||||
pressBackspace();
|
||||
assertEquals("foo\n\n- [ ] bar\n");
|
||||
|
||||
sendKeys(KEYCODE_SPACE, KEYCODE_SPACE);
|
||||
sendCheckboxKeys();
|
||||
sendKeys(KEYCODE_B, KEYCODE_A, KEYCODE_R);
|
||||
assertEquals("foo\n\n- [ ] bar\n - [ ] bar");
|
||||
|
||||
pressEnter();
|
||||
assertEquals("foo\n\n- [ ] bar\n - [ ] bar\n - [ ] ");
|
||||
|
||||
pressBackspace();
|
||||
assertEquals("foo\n\n- [ ] bar\n - [ ] bar\n- [ ] ");
|
||||
|
||||
pressBackspace(6, 33);
|
||||
assertEquals("foo\n\n- [ ] bar\n - [ ] bar\n");
|
||||
|
||||
sendKeys(KEYCODE_SPACE, KEYCODE_SPACE, KEYCODE_1, KEYCODE_PERIOD, KEYCODE_SPACE, KEYCODE_Q, KEYCODE_U, KEYCODE_X);
|
||||
assertEquals("foo\n\n- [ ] bar\n - [ ] bar\n 1. qux");
|
||||
|
||||
pressEnter();
|
||||
assertEquals("foo\n\n- [ ] bar\n - [ ] bar\n 1. qux\n 2. ");
|
||||
|
||||
pressEnter(14);
|
||||
pressBackspace(6, 21);
|
||||
assertEquals("foo\n\n- [ ] bar\n\n - [ ] bar\n 1. qux\n 2. ");
|
||||
|
||||
sendKeys(KEYCODE_SPACE, KEYCODE_SPACE, KEYCODE_STAR);
|
||||
pressBackspace();
|
||||
sendKeys(KEYCODE_STAR, KEYCODE_SPACE, KEYCODE_A, KEYCODE_B, KEYCODE_C);
|
||||
assertEquals("foo\n\n- [ ] bar\n * abc\n - [ ] bar\n 1. qux\n 2. ");
|
||||
}
|
||||
|
||||
|
||||
// Convenience methods
|
||||
|
||||
private void sendCheckboxKeys() {
|
||||
sendKeys(KEYCODE_MINUS, KEYCODE_SPACE, KEYCODE_CTRL_LEFT, KEYCODE_LEFT_BRACKET, KEYCODE_SPACE, KEYCODE_RIGHT_BRACKET, KEYCODE_SPACE);
|
||||
}
|
||||
|
||||
private void assertEquals(@NonNull CharSequence expected) {
|
||||
assertEquals(expected.toString(), editText.getText().toString());
|
||||
}
|
||||
|
||||
private void pressEnter(@SuppressWarnings("SameParameterValue") int atPosition) {
|
||||
this.editText.setSelection(atPosition);
|
||||
pressEnter();
|
||||
}
|
||||
|
||||
private void pressEnter() {
|
||||
this.editText.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
|
||||
}
|
||||
|
||||
private void pressBackspace(@SuppressWarnings("SameParameterValue") int times, int startPosition) {
|
||||
if (times > startPosition) {
|
||||
throw new IllegalArgumentException("startPosition must be bigger or equal to times");
|
||||
}
|
||||
while (times > 0) {
|
||||
times--;
|
||||
pressBackspace(startPosition--);
|
||||
}
|
||||
}
|
||||
|
||||
private void pressBackspace(int atPosition) {
|
||||
this.editText.setSelection(atPosition);
|
||||
pressBackspace();
|
||||
}
|
||||
|
||||
private void pressBackspace() {
|
||||
this.editText.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
|
||||
}
|
||||
|
||||
private void sendKeys(int... keyCodes) {
|
||||
int release = -1;
|
||||
for (int k : keyCodes) {
|
||||
if (release != -1) {
|
||||
this.editText.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, release));
|
||||
release = -1;
|
||||
}
|
||||
switch (k) {
|
||||
case KeyEvent.KEYCODE_CTRL_LEFT:
|
||||
case KeyEvent.KEYCODE_CTRL_RIGHT: {
|
||||
release = k;
|
||||
}
|
||||
}
|
||||
this.editText.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, k));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
sdk=22, 30
|
|
@ -1,2 +1 @@
|
|||
include ':app'
|
||||
include ':markdown'
|
||||
|
|
Loading…
Reference in a new issue