From 20214ce32302b48e83404f66ba9ccb79c64830d0 Mon Sep 17 00:00:00 2001 From: scria1000 <91804886+scria1000@users.noreply.github.com> Date: Thu, 3 Nov 2022 13:07:25 +0300 Subject: Superscript parser overhaul (#1162) * Superscript parser overhaul * Make superscript work with tables * Fix some issues with Table compatibility * Some bug fixes * Re-enable Autolink --- app/build.gradle | 1 - .../activities/CommentActivity.java | 6 - .../activities/FullMarkdownActivity.java | 6 - .../infinityforreddit/activities/WikiActivity.java | 6 - .../CommentsListingRecyclerViewAdapter.java | 6 - .../adapters/CommentsRecyclerViewAdapter.java | 6 - .../adapters/MessageRecyclerViewAdapter.java | 17 +- .../adapters/PostDetailRecyclerViewAdapter.java | 6 - .../PrivateMessagesDetailRecyclerViewAdapter.java | 16 +- .../adapters/RulesRecyclerViewAdapter.java | 6 - .../fragments/SidebarFragment.java | 6 - .../infinityforreddit/markdown/MarkdownUtils.java | 9 +- .../infinityforreddit/markdown/Superscript.java | 21 +++ .../SuperscriptClosingInlineProcessor.java | 51 +++++ .../markdown/SuperscriptInlineProcessor.java | 32 ---- .../markdown/SuperscriptOpening.java | 17 ++ .../markdown/SuperscriptOpeningBracket.java | 31 ++++ .../SuperscriptOpeningInlineProcessor.java | 50 +++++ .../markdown/SuperscriptOpeningStorage.java | 38 ++++ .../markdown/SuperscriptPlugin.java | 206 +++++++++++++++++++++ .../markdown/SuperscriptSpan.java | 34 ++++ .../infinityforreddit/utils/Utils.java | 60 ------ 22 files changed, 453 insertions(+), 178 deletions(-) create mode 100644 app/src/main/java/ml/docilealligator/infinityforreddit/markdown/Superscript.java create mode 100644 app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptClosingInlineProcessor.java delete mode 100644 app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptInlineProcessor.java create mode 100644 app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptOpening.java create mode 100644 app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptOpeningBracket.java create mode 100644 app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptOpeningInlineProcessor.java create mode 100644 app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptOpeningStorage.java create mode 100644 app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptPlugin.java create mode 100644 app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptSpan.java diff --git a/app/build.gradle b/app/build.gradle index 9afd4eb1..76332b12 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -161,7 +161,6 @@ dependencies { implementation "io.noties.markwon:linkify:$markwonVersion" implementation "io.noties.markwon:recycler-table:$markwonVersion" implementation "io.noties.markwon:simple-ext:$markwonVersion" - implementation "io.noties.markwon:html:$markwonVersion" implementation "io.noties.markwon:inline-parser:$markwonVersion" implementation 'com.atlassian.commonmark:commonmark-ext-gfm-tables:0.14.0' implementation 'me.saket:better-link-movement-method:2.2.0' diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/activities/CommentActivity.java b/app/src/main/java/ml/docilealligator/infinityforreddit/activities/CommentActivity.java index c9474bb2..bb2e43de 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/activities/CommentActivity.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/activities/CommentActivity.java @@ -178,12 +178,6 @@ public class CommentActivity extends BaseActivity implements UploadImageEnabledA binding.commentContentMarkdownView.setNestedScrollingEnabled(false); int linkColor = mCustomThemeWrapper.getLinkColor(); MarkwonPlugin miscPlugin = new AbstractMarkwonPlugin() { - @NonNull - @Override - public String processMarkdown(@NonNull String markdown) { - return Utils.fixSuperScript(markdown); - } - @Override public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { if (contentTypeface != null) { diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/activities/FullMarkdownActivity.java b/app/src/main/java/ml/docilealligator/infinityforreddit/activities/FullMarkdownActivity.java index 2ded838c..81276e23 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/activities/FullMarkdownActivity.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/activities/FullMarkdownActivity.java @@ -117,12 +117,6 @@ public class FullMarkdownActivity extends BaseActivity { int spoilerBackgroundColor = markdownColor | 0xFF000000; int linkColor = mCustomThemeWrapper.getLinkColor(); MarkwonPlugin miscPlugin = new AbstractMarkwonPlugin() { - @NonNull - @Override - public String processMarkdown(@NonNull String markdown) { - return Utils.fixSuperScript(markdown); - } - @Override public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { if (typeface != null) { diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/activities/WikiActivity.java b/app/src/main/java/ml/docilealligator/infinityforreddit/activities/WikiActivity.java index 6cb65b28..00576c85 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/activities/WikiActivity.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/activities/WikiActivity.java @@ -149,12 +149,6 @@ public class WikiActivity extends BaseActivity { int spoilerBackgroundColor = markdownColor | 0xFF000000; int linkColor = mCustomThemeWrapper.getLinkColor(); MarkwonPlugin miscPlugin = new AbstractMarkwonPlugin() { - @NonNull - @Override - public String processMarkdown(@NonNull String markdown) { - return Utils.fixSuperScript(markdown); - } - @Override public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { textView.setTextColor(markdownColor); diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/CommentsListingRecyclerViewAdapter.java b/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/CommentsListingRecyclerViewAdapter.java index 1503a469..d7432db6 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/CommentsListingRecyclerViewAdapter.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/CommentsListingRecyclerViewAdapter.java @@ -138,12 +138,6 @@ public class CommentsListingRecyclerViewAdapter extends PagedListAdapter { @@ -109,21 +106,10 @@ public class MessageRecyclerViewAdapter extends PagedListAdapter { - plugin.excludeInlineProcessor(AutolinkInlineProcessor.class); plugin.excludeInlineProcessor(HtmlInlineProcessor.class); plugin.excludeInlineProcessor(BangInlineProcessor.class); - plugin.addInlineProcessor(new SuperscriptInlineProcessor()); - })) - .usePlugin(HtmlPlugin.create(plugin -> { - plugin.excludeDefaults(true).addHandler(new SuperScriptHandler()); })) .usePlugin(new AbstractMarkwonPlugin() { - @NonNull - @Override - public String processMarkdown(@NonNull String markdown) { - return Utils.fixSuperScript(markdown); - } - @Override public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { builder.linkResolver((view, link) -> { @@ -139,6 +125,7 @@ public class MessageRecyclerViewAdapter extends PagedListAdapter { - plugin.excludeInlineProcessor(AutolinkInlineProcessor.class); plugin.excludeInlineProcessor(HtmlInlineProcessor.class); plugin.excludeInlineProcessor(BangInlineProcessor.class); - plugin.addInlineProcessor(new SuperscriptInlineProcessor()); - })) - .usePlugin(HtmlPlugin.create(plugin -> { - plugin.excludeDefaults(true).addHandler(new SuperScriptHandler()); })) .usePlugin(new AbstractMarkwonPlugin() { - @NonNull - @Override - public String processMarkdown(@NonNull String markdown) { - return Utils.fixSuperScript(markdown); - } - @Override public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { if (mViewPrivateMessagesActivity.contentTypeface != null) { @@ -122,6 +109,7 @@ public class PrivateMessagesDetailRecyclerViewAdapter extends RecyclerView.Adapt builder.linkColor(customThemeWrapper.getLinkColor()); } }) + .usePlugin(SuperscriptPlugin.create()) .usePlugin(StrikethroughPlugin.create()) .usePlugin(SpoilerParserPlugin.create(commentColor, commentColor | 0xFF000000)) .usePlugin(RedditHeadingPlugin.create()) diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/RulesRecyclerViewAdapter.java b/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/RulesRecyclerViewAdapter.java index c7d32b93..1fa0a8a5 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/RulesRecyclerViewAdapter.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/adapters/RulesRecyclerViewAdapter.java @@ -53,12 +53,6 @@ public class RulesRecyclerViewAdapter extends RecyclerView.Adapter { - plugin.excludeInlineProcessor(AutolinkInlineProcessor.class); plugin.excludeInlineProcessor(HtmlInlineProcessor.class); plugin.excludeInlineProcessor(BangInlineProcessor.class); - plugin.addInlineProcessor(new SuperscriptInlineProcessor()); - })) - .usePlugin(HtmlPlugin.create(plugin -> { - plugin.excludeDefaults(true).addHandler(new SuperScriptHandler()); })) .usePlugin(miscPlugin) + .usePlugin(SuperscriptPlugin.create()) .usePlugin(SpoilerParserPlugin.create(markdownColor, spoilerBackgroundColor)) .usePlugin(RedditHeadingPlugin.create()) .usePlugin(StrikethroughPlugin.create()) @@ -68,7 +62,6 @@ public class MarkdownUtils { @Nullable BetterLinkMovementMethod.OnLinkLongClickListener onLinkLongClickListener) { return Markwon.builder(context) .usePlugin(MarkwonInlineParserPlugin.create(plugin -> { - plugin.excludeInlineProcessor(AutolinkInlineProcessor.class); plugin.excludeInlineProcessor(HtmlInlineProcessor.class); plugin.excludeInlineProcessor(BangInlineProcessor.class); })) diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/Superscript.java b/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/Superscript.java new file mode 100644 index 00000000..a2cb7b74 --- /dev/null +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/Superscript.java @@ -0,0 +1,21 @@ +package ml.docilealligator.infinityforreddit.markdown; + +import org.commonmark.node.CustomNode; +import org.commonmark.node.Visitor; + +public class Superscript extends CustomNode { + private boolean isBracketed; + + @Override + public void accept(Visitor visitor) { + visitor.visit(this); + } + + public boolean isBracketed() { + return isBracketed; + } + + public void setBracketed(boolean bracketed) { + isBracketed = bracketed; + } +} diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptClosingInlineProcessor.java b/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptClosingInlineProcessor.java new file mode 100644 index 00000000..8bdf9c44 --- /dev/null +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptClosingInlineProcessor.java @@ -0,0 +1,51 @@ +package ml.docilealligator.infinityforreddit.markdown; + +import static io.noties.markwon.inlineparser.InlineParserUtils.mergeChildTextNodes; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.commonmark.node.Node; + +import io.noties.markwon.inlineparser.InlineProcessor; + +public class SuperscriptClosingInlineProcessor extends InlineProcessor { + @NonNull + private final SuperscriptOpeningStorage superscriptOpeningStorage; + + public SuperscriptClosingInlineProcessor(@NonNull SuperscriptOpeningStorage superscriptOpeningStorage) { + this.superscriptOpeningStorage = superscriptOpeningStorage; + } + + @Override + public char specialCharacter() { + return ')'; + } + + @Nullable + @Override + protected Node parse() { + SuperscriptOpeningBracket superscriptOpening = superscriptOpeningStorage.pop(block); + if (superscriptOpening == null) { + return null; + } + index++; + + Superscript superscript = new Superscript(); + superscript.setBracketed(true); + Node node = superscriptOpening.node.getNext(); + while (node != null) { + Node next = node.getNext(); + superscript.appendChild(node); + node = next; + } + + // Process delimiters such as emphasis inside spoiler + processDelimiters(superscriptOpening.previousDelimiter); + mergeChildTextNodes(superscript); + // We don't need the corresponding text node anymore, we turned it into a spoiler node + superscriptOpening.node.unlink(); + + return superscript; + } +} diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptInlineProcessor.java b/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptInlineProcessor.java deleted file mode 100644 index 93c9ab10..00000000 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptInlineProcessor.java +++ /dev/null @@ -1,32 +0,0 @@ -package ml.docilealligator.infinityforreddit.markdown; - -import androidx.annotation.Nullable; - -import org.commonmark.node.HtmlInline; -import org.commonmark.node.Node; - -import java.util.regex.Pattern; - -import io.noties.markwon.inlineparser.InlineProcessor; - -public class SuperscriptInlineProcessor extends InlineProcessor { - private static final Pattern HTML_TAG = Pattern.compile("^", Pattern.CASE_INSENSITIVE); - - @Override - public char specialCharacter() { - return '<'; - } - - @Nullable - @Override - protected Node parse() { - String m = match(HTML_TAG); - if (m != null) { - HtmlInline node = new HtmlInline(); - node.setLiteral(m); - return node; - } else { - return null; - } - } -} diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptOpening.java b/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptOpening.java new file mode 100644 index 00000000..9b8107cb --- /dev/null +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptOpening.java @@ -0,0 +1,17 @@ +package ml.docilealligator.infinityforreddit.markdown; + +import org.commonmark.node.Node; + +public class SuperscriptOpening { + /** + * Node that contains non-bracketed superscript opening markdown ({@code ^}). + */ + public final Node node; + + public final Integer start; + + public SuperscriptOpening(Node node, int start) { + this.node = node; + this.start = start; + } +} diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptOpeningBracket.java b/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptOpeningBracket.java new file mode 100644 index 00000000..e47d43b0 --- /dev/null +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptOpeningBracket.java @@ -0,0 +1,31 @@ +package ml.docilealligator.infinityforreddit.markdown; + +import org.commonmark.internal.Delimiter; +import org.commonmark.node.Node; + +public class SuperscriptOpeningBracket { + /** + * Node that contains superscript opening bracket markdown ({@code ^(}). + */ + public final Node node; + + /** + * Previous superscript opening bracket. + */ + public final SuperscriptOpeningBracket previous; + + /** + * Previous delimiter (emphasis, etc) before this bracket. + */ + public final Delimiter previousDelimiter; + + public final Integer start; + + public SuperscriptOpeningBracket(Node node, SuperscriptOpeningBracket previous, Delimiter previousDelimiter) { + this.node = node; + this.previous = previous; + this.previousDelimiter = previousDelimiter; + this.start = null; + } +} + diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptOpeningInlineProcessor.java b/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptOpeningInlineProcessor.java new file mode 100644 index 00000000..1224df85 --- /dev/null +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptOpeningInlineProcessor.java @@ -0,0 +1,50 @@ +package ml.docilealligator.infinityforreddit.markdown; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.commonmark.node.Node; +import org.commonmark.node.Text; + +import io.noties.markwon.inlineparser.InlineProcessor; + +public class SuperscriptOpeningInlineProcessor extends InlineProcessor { + @NonNull + private final SuperscriptOpeningStorage superscriptOpeningStorage; + + public SuperscriptOpeningInlineProcessor(@NonNull SuperscriptOpeningStorage superscriptOpeningStorage) { + this.superscriptOpeningStorage = superscriptOpeningStorage; + } + + @Override + public char specialCharacter() { + return '^'; + } + + @Nullable + @Override + protected Node parse() { + index++; + char c = peek(); + if (c != '\0' && !Character.isWhitespace(c)) { + if (c == '(') { + index++; + Text node = text("^("); + superscriptOpeningStorage.add(block, node, lastDelimiter()); + return node; + } + + if (lastDelimiter() != null && lastDelimiter().canOpen && block.getLastChild() != null) { + if (lastDelimiter().node == this.block.getLastChild()) { + if (lastDelimiter().delimiterChar == peek()) { + index--; + return null; + } + } + } + + return new Superscript(); + } + return null; + } +} diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptOpeningStorage.java b/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptOpeningStorage.java new file mode 100644 index 00000000..ce0db58c --- /dev/null +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptOpeningStorage.java @@ -0,0 +1,38 @@ +package ml.docilealligator.infinityforreddit.markdown; + +import androidx.annotation.Nullable; + +import org.commonmark.internal.Delimiter; +import org.commonmark.node.Node; + +public class SuperscriptOpeningStorage { + @Nullable + private SuperscriptOpeningBracket lastBracket; + private Node currentBlock; + + public void clear() { + lastBracket = null; + } + + public void add(Node block, Node node, Delimiter lastDelimiter) { + updateBlock(block); + lastBracket = new SuperscriptOpeningBracket(node, lastBracket, lastDelimiter); + } + + @Nullable + public SuperscriptOpeningBracket pop(Node block) { + updateBlock(block); + SuperscriptOpeningBracket opening = lastBracket; + if (opening != null) { + lastBracket = opening.previous; + } + return opening; + } + + private void updateBlock(Node block) { + if (block != currentBlock) { + clear(); + } + currentBlock = block; + } +} diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptPlugin.java b/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptPlugin.java new file mode 100644 index 00000000..1a5f3526 --- /dev/null +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptPlugin.java @@ -0,0 +1,206 @@ +package ml.docilealligator.infinityforreddit.markdown; + +import android.text.Spannable; +import android.text.Spanned; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import org.commonmark.ext.gfm.tables.TableCell; +import org.commonmark.node.Node; +import org.commonmark.node.Paragraph; + +import java.util.ArrayList; +import java.util.List; + +import io.noties.markwon.AbstractMarkwonPlugin; +import io.noties.markwon.MarkwonSpansFactory; +import io.noties.markwon.MarkwonVisitor; +import io.noties.markwon.core.spans.CodeSpan; +import io.noties.markwon.core.spans.TextViewSpan; +import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin; + +public class SuperscriptPlugin extends AbstractMarkwonPlugin { + private final SuperscriptOpeningStorage superscriptOpeningBracketStorage; + private final List superscriptOpeningList; + + SuperscriptPlugin() { + this.superscriptOpeningBracketStorage = new SuperscriptOpeningStorage(); + this.superscriptOpeningList = new ArrayList<>(); + } + + @NonNull + public static SuperscriptPlugin create() { + return new SuperscriptPlugin(); + } + + private static char peek(int index, CharSequence input) { + return index >= 0 && index < input.length() ? input.charAt(index) : '\0'; + } + + private static List getSpans(Spannable spannable, int start, int end) { + var spanArray = spannable.getSpans(start, end, Object.class); + List spanList = new ArrayList<>(); + for (int i = spanArray.length - 1; i >= 0; i--) { + Object span = spanArray[i]; + int spanStart = spannable.getSpanStart(span); + int spanEnd = spannable.getSpanEnd(span); + int spanFlags = spannable.getSpanFlags(span); + spanList.add(new SpanInfo(span, spanStart, spanEnd, spanFlags)); + } + return spanList; + } + + private static SpanInfo matchSuperscriptAtPosition(List spans, int value) { + for (var span : spans) + if (span.what.getClass() == SuperscriptSpan.class && !((SuperscriptSpan) span.what).isBracketed && span.start <= value && value <= span.end) + return span; + return null; + } + + private static SpanInfo matchSpanAtPosition(List spans, int value, Object spanClass) { + for (var span : spans) + if (span.what.getClass() == spanClass && span.start <= value && value <= span.end) + return span; + return null; + } + + private static SpanInfo matchNonTextSpanAtBoundary(List spans, int value) { + for (var span : spans) + if ((span.end == value || span.start == value) && span.what.getClass() != CodeSpan.class && span.what.getClass() != SuperscriptSpan.class && span.what.getClass() != TextViewSpan.class) + return span; + return null; + } + + @Override + public void configure(@NonNull Registry registry) { + registry.require(MarkwonInlineParserPlugin.class, plugin -> { + plugin.factoryBuilder().addInlineProcessor(new SuperscriptOpeningInlineProcessor(superscriptOpeningBracketStorage)); + plugin.factoryBuilder().addInlineProcessor(new SuperscriptClosingInlineProcessor(superscriptOpeningBracketStorage)); + } + ); + } + + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + builder.setFactory(Superscript.class, (config, renderProps) -> new SuperscriptSpan(true)); + } + + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + builder.on(Superscript.class, new MarkwonVisitor.NodeVisitor<>() { + int depth = 0; + + @Override + public void visit(@NonNull MarkwonVisitor visitor, @NonNull Superscript superscript) { + int start = visitor.length(); + + if (!superscript.isBracketed()) { + visitor.builder().setSpan(new SuperscriptSpan(false), start, start + 1); // Workaround for Table Plugin + superscriptOpeningList.add(new SuperscriptOpening(superscript, start)); + return; + } + + depth++; + visitor.visitChildren(superscript); + depth--; + if (depth == 0) { + int end = visitor.builder().length(); + var spans = visitor.builder().getSpans(start, end); + for (var span : spans) { + if (span.what instanceof CodeSpan) { + if (span.end <= end) { + visitor.builder().setSpan(new SuperscriptSpan(true), start, span.start); + } + start = span.end; + } + } + if (start < end) { + visitor.setSpansForNode(superscript, start); + } + } + } + }); + } + + @Override + public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) { + superscriptOpeningBracketStorage.clear(); + } + + @Override + public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { + if (superscriptOpeningList.size() == 0 || !(markdown instanceof Spannable)) { + return; + } + + var spannable = (Spannable) markdown; + var spans = getSpans(spannable, 0, spannable.length()); + final String text = spannable.toString(); + + outerLoop: + for (int i = 0; i < superscriptOpeningList.size(); i++) { + SuperscriptOpening opening = superscriptOpeningList.get(i); + SuperscriptOpening nextOpening = i + 1 < superscriptOpeningList.size() ? superscriptOpeningList.get(i + 1) : null; + + // Workaround for Table Plugin + var superscriptMarker = matchSuperscriptAtPosition(spans, opening.start); + if (superscriptMarker == null) + return; + spannable.removeSpan(superscriptMarker.what); + spans.remove(superscriptMarker); + + boolean isNextOpeningOfLocalNode = nextOpening != null && opening.node.getParent().equals(nextOpening.node.getParent()); + if (opening.start >= text.length() || (matchSpanAtPosition(spans, opening.start, CodeSpan.class) == null && Character.isWhitespace(text.charAt(opening.start))) || (isNextOpeningOfLocalNode && opening.start.equals(nextOpening.start))) { + superscriptOpeningList.remove(i); + i--; + continue; + } + + boolean isChildOfDelimited = !(opening.node.getParent() == null || opening.node.getParent() instanceof Paragraph || opening.node.getParent() instanceof TableCell); + int openingStart = opening.start; + for (int j = opening.start; j <= text.length(); j++) { + char currentChar = peek(j, text); + SpanInfo codeSpanAtPosition = matchSpanAtPosition(spans, j, CodeSpan.class); + SpanInfo nonTextSpanAtBoundary = matchNonTextSpanAtBoundary(spans, j); + // When we reach the end position of, for example, an Emphasis + // Check whether the superscript originated from inside this Emphasis + // If so, stop further spanning of the current Superscript + boolean isInsideDelimited = nonTextSpanAtBoundary != null && openingStart != j && j == nonTextSpanAtBoundary.end && (openingStart > nonTextSpanAtBoundary.start || isChildOfDelimited); + if (codeSpanAtPosition != null) { + if (openingStart < j) { + spannable.setSpan(new SuperscriptSpan(false), openingStart, j, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + //Skip to end of CodeSpan + j = codeSpanAtPosition.end; + currentChar = peek(j, text); + if (currentChar == '\0' || Character.isWhitespace(currentChar) || (isNextOpeningOfLocalNode && j == nextOpening.start) || isInsideDelimited) { + superscriptOpeningList.remove(i); + i--; + continue outerLoop; + } + openingStart = j; + } else if (currentChar == '\0' || Character.isWhitespace(currentChar) || isInsideDelimited) { + spannable.setSpan(new SuperscriptSpan(false), openingStart, j, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + superscriptOpeningList.remove(i); + i--; + continue outerLoop; + } + } + } + } + + private static class SpanInfo { + public final Object what; + public final int start; + public final int end; + public final int flags; + + private SpanInfo(Object what, int start, int end, int flags) { + this.what = what; + this.start = start; + this.end = end; + this.flags = flags; + } + } +} diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptSpan.java b/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptSpan.java new file mode 100644 index 00000000..8ea30317 --- /dev/null +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/markdown/SuperscriptSpan.java @@ -0,0 +1,34 @@ +package ml.docilealligator.infinityforreddit.markdown; + +import android.text.TextPaint; +import android.text.style.MetricAffectingSpan; + +import androidx.annotation.NonNull; + +public class SuperscriptSpan extends MetricAffectingSpan { + private static final float SCRIPT_DEF_TEXT_SIZE_RATIO = .75F; + public final boolean isBracketed; + + public SuperscriptSpan() { + this.isBracketed = false; + } + + public SuperscriptSpan(boolean isBracketed) { + this.isBracketed = isBracketed; + } + + @Override + public void updateDrawState(TextPaint tp) { + apply(tp); + } + + @Override + public void updateMeasureState(@NonNull TextPaint tp) { + apply(tp); + } + + private void apply(TextPaint paint) { + paint.setTextSize(paint.getTextSize() * SCRIPT_DEF_TEXT_SIZE_RATIO); + paint.baselineShift += (int) (paint.ascent() / 2); + } +} diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/utils/Utils.java b/app/src/main/java/ml/docilealligator/infinityforreddit/utils/Utils.java index a663cfb4..39aadc17 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/utils/Utils.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/utils/Utils.java @@ -83,69 +83,9 @@ public final class Utils { regexed = REGEX_PATTERNS[1].matcher(regexed).replaceAll("[$0](https://www.reddit.com/$0)"); regexed = REGEX_PATTERNS[2].matcher(regexed).replaceAll("^"); - //return fixSuperScript(regexed); - // We don't want to fix super scripts here because we need the original markdown later for editing posts return regexed; } - public static String fixSuperScript(String regexedMarkdown) { - StringBuilder regexed = new StringBuilder(regexedMarkdown); - boolean hasBracket = false; - int nCarets = 0; - int newLines = 0; - for (int i = 0; i < regexed.length(); i++) { - char currentChar = regexed.charAt(i); - if (hasBracket && currentChar == '\n') { - newLines++; - if (newLines > 1) { - hasBracket = false; - nCarets = 0; - newLines = 0; - } - } else if (currentChar == '^') { - if (!(i > 0 && regexed.charAt(i - 1) == '\\')) { - if (nCarets == 0 && i < regexed.length() - 1 && regexed.charAt(i + 1) == '(') { - regexed.replace(i, i + 2, ""); - hasBracket = true; - } else { - regexed.replace(i, i + 1, ""); - } - nCarets++; - } - } else if (hasBracket && currentChar == ')') { - if (i > 0 && regexed.charAt(i - 1) == '\\') { - hasBracket = false; - nCarets--; - continue; - } - hasBracket = false; - regexed.replace(i, i + 1, ""); - nCarets--; - } else if (!hasBracket && currentChar == '\n') { - for (int j = 0; j < nCarets; j++) { - regexed.insert(i, ""); - i += 6; - } - nCarets = 0; - } else if (!hasBracket && Character.isWhitespace(currentChar)) { - for (int j = 0; j < nCarets; j++) { - regexed.insert(i, ""); - i += 6; - } - nCarets = 0; - } else { - newLines = 0; - } - } - if (!hasBracket) { - for (int j = 0; j < nCarets; j++) { - regexed.append(""); - } - } - - return regexed.toString(); - } - public static String parseInlineGifInComments(String markdown) { StringBuilder markdownStringBuilder = new StringBuilder(markdown); Pattern inlineGifPattern = REGEX_PATTERNS[3]; -- cgit v1.2.3