From 50d331bc87d76b775313995facce8b6d38750db0 Mon Sep 17 00:00:00 2001
From: chengjunzhang61 <chengjun@finerpoint.com>
Date: Wed, 8 Dec 2021 10:00:03 -0500
Subject: [PATCH] Create iframe plugin

---
 markwon-iframe-ext/.gitignore                 |   1 +
 markwon-iframe-ext/build.gradle               |  48 +++++
 .../iframe/ext/ExampleInstrumentedTest.java   |  26 +++
 .../src/main/AndroidManifest.xml              |   5 +
 .../markwon/iframe/ext/IFrameGroupNode.java   |   7 +
 .../noties/markwon/iframe/ext/IFrameNode.java |  19 ++
 .../markwon/iframe/ext/IFramePlugIn.java      |  44 +++++
 .../markwon/iframe/ext/IFrameProcessor.java   | 166 ++++++++++++++++++
 .../markwon/iframe/ext/IFrameUtils.java       |  54 ++++++
 .../markwon/iframe/ext/ExampleUnitTest.java   |  17 ++
 10 files changed, 387 insertions(+)
 create mode 100644 markwon-iframe-ext/.gitignore
 create mode 100644 markwon-iframe-ext/build.gradle
 create mode 100644 markwon-iframe-ext/src/androidTest/java/io/noties/markwon/iframe/ext/ExampleInstrumentedTest.java
 create mode 100644 markwon-iframe-ext/src/main/AndroidManifest.xml
 create mode 100644 markwon-iframe-ext/src/main/java/io/noties/markwon/iframe/ext/IFrameGroupNode.java
 create mode 100644 markwon-iframe-ext/src/main/java/io/noties/markwon/iframe/ext/IFrameNode.java
 create mode 100644 markwon-iframe-ext/src/main/java/io/noties/markwon/iframe/ext/IFramePlugIn.java
 create mode 100644 markwon-iframe-ext/src/main/java/io/noties/markwon/iframe/ext/IFrameProcessor.java
 create mode 100644 markwon-iframe-ext/src/main/java/io/noties/markwon/iframe/ext/IFrameUtils.java
 create mode 100644 markwon-iframe-ext/src/test/java/io/noties/markwon/iframe/ext/ExampleUnitTest.java

diff --git a/markwon-iframe-ext/.gitignore b/markwon-iframe-ext/.gitignore
new file mode 100644
index 00000000..42afabfd
--- /dev/null
+++ b/markwon-iframe-ext/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/markwon-iframe-ext/build.gradle b/markwon-iframe-ext/build.gradle
new file mode 100644
index 00000000..c24bf0a7
--- /dev/null
+++ b/markwon-iframe-ext/build.gradle
@@ -0,0 +1,48 @@
+plugins {
+    id 'com.android.library'
+}
+
+android {
+    compileSdkVersion 31
+
+    defaultConfig {
+        minSdkVersion 21
+        targetSdkVersion 31
+        versionCode 1
+        versionName "1.0"
+
+        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+        consumerProguardFiles "consumer-rules.pro"
+    }
+
+    buildTypes {
+        release {
+            minifyEnabled false
+            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+        }
+    }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
+}
+
+dependencies {
+
+    api project(':markwon-core')
+
+    deps.with {
+        // add a compileOnly dependency, so if this artifact is present
+        // we will try to obtain a SpanFactory for a Strikethrough node and use
+        // it to be consistent with markdown (please note that we do not use markwon plugin
+        // for that in case if different implementation is used)
+        compileOnly it['commonmark-strikethrough']
+
+        testImplementation it['ix-java']
+    }
+
+    deps.test.with {
+        testImplementation it['junit']
+        testImplementation it['robolectric']
+    }
+}
\ No newline at end of file
diff --git a/markwon-iframe-ext/src/androidTest/java/io/noties/markwon/iframe/ext/ExampleInstrumentedTest.java b/markwon-iframe-ext/src/androidTest/java/io/noties/markwon/iframe/ext/ExampleInstrumentedTest.java
new file mode 100644
index 00000000..54a07579
--- /dev/null
+++ b/markwon-iframe-ext/src/androidTest/java/io/noties/markwon/iframe/ext/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package io.noties.markwon.iframe.ext;
+
+import android.content.Context;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+    @Test
+    public void useAppContext() {
+        // Context of the app under test.
+        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
+        assertEquals("io.noties.markwon.iframe.ext.test", appContext.getPackageName());
+    }
+}
\ No newline at end of file
diff --git a/markwon-iframe-ext/src/main/AndroidManifest.xml b/markwon-iframe-ext/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..880a9db8
--- /dev/null
+++ b/markwon-iframe-ext/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="io.noties.markwon.iframe.ext">
+
+</manifest>
\ No newline at end of file
diff --git a/markwon-iframe-ext/src/main/java/io/noties/markwon/iframe/ext/IFrameGroupNode.java b/markwon-iframe-ext/src/main/java/io/noties/markwon/iframe/ext/IFrameGroupNode.java
new file mode 100644
index 00000000..e752277c
--- /dev/null
+++ b/markwon-iframe-ext/src/main/java/io/noties/markwon/iframe/ext/IFrameGroupNode.java
@@ -0,0 +1,7 @@
+package io.noties.markwon.iframe.ext;
+
+
+import org.commonmark.node.CustomNode;
+
+public class IFrameGroupNode extends CustomNode {
+}
diff --git a/markwon-iframe-ext/src/main/java/io/noties/markwon/iframe/ext/IFrameNode.java b/markwon-iframe-ext/src/main/java/io/noties/markwon/iframe/ext/IFrameNode.java
new file mode 100644
index 00000000..0dea2fda
--- /dev/null
+++ b/markwon-iframe-ext/src/main/java/io/noties/markwon/iframe/ext/IFrameNode.java
@@ -0,0 +1,19 @@
+package io.noties.markwon.iframe.ext;
+
+import androidx.annotation.NonNull;
+
+import org.commonmark.node.CustomNode;
+
+public class IFrameNode extends CustomNode {
+
+    public static final String DELIMITER_STRING = "![";
+    private final String link;
+    public IFrameNode(@NonNull String link) {
+        this.link = link;
+    }
+
+    @NonNull
+    public String link() {
+        return link;
+    }
+}
\ No newline at end of file
diff --git a/markwon-iframe-ext/src/main/java/io/noties/markwon/iframe/ext/IFramePlugIn.java b/markwon-iframe-ext/src/main/java/io/noties/markwon/iframe/ext/IFramePlugIn.java
new file mode 100644
index 00000000..28a964d0
--- /dev/null
+++ b/markwon-iframe-ext/src/main/java/io/noties/markwon/iframe/ext/IFramePlugIn.java
@@ -0,0 +1,44 @@
+package io.noties.markwon.iframe.ext;
+
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+
+import org.commonmark.parser.Parser;
+
+import io.noties.markwon.AbstractMarkwonPlugin;
+import io.noties.markwon.MarkwonVisitor;
+
+public class IFramePlugIn extends AbstractMarkwonPlugin {
+
+    @NonNull
+    public static IFramePlugIn create() {
+        return new IFramePlugIn();
+    }
+
+    @Override
+    public void configureParser(@NonNull Parser.Builder builder) {
+        builder.customDelimiterProcessor(IFrameProcessor.create());
+    }
+
+    @Override
+    public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
+        builder.on(IFrameNode.class, new MarkwonVisitor.NodeVisitor<IFrameNode>() {
+            @Override
+            public void visit(@NonNull MarkwonVisitor visitor, @NonNull IFrameNode iFrameNode) {
+
+                final String link = iFrameNode.link();
+                if (!TextUtils.isEmpty(link)) {
+                    visitor.builder().append(link);
+                    visitor.builder().append(' ');
+                }
+            }
+        });
+    }
+
+    @NonNull
+    @Override
+    public String processMarkdown(@NonNull String markdown) {
+        return IFrameProcessor.prepare(markdown);
+    }
+}
diff --git a/markwon-iframe-ext/src/main/java/io/noties/markwon/iframe/ext/IFrameProcessor.java b/markwon-iframe-ext/src/main/java/io/noties/markwon/iframe/ext/IFrameProcessor.java
new file mode 100644
index 00000000..ff4a78eb
--- /dev/null
+++ b/markwon-iframe-ext/src/main/java/io/noties/markwon/iframe/ext/IFrameProcessor.java
@@ -0,0 +1,166 @@
+package io.noties.markwon.iframe.ext;
+
+import static io.noties.markwon.iframe.ext.IFrameUtils.getDesmosId;
+import static io.noties.markwon.iframe.ext.IFrameUtils.getVimeoVideoId;
+import static io.noties.markwon.iframe.ext.IFrameUtils.getYoutubeVideoId;
+
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+
+import org.commonmark.node.Node;
+import org.commonmark.node.Text;
+import org.commonmark.parser.delimiter.DelimiterProcessor;
+import org.commonmark.parser.delimiter.DelimiterRun;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class IFrameProcessor implements DelimiterProcessor {
+
+    private static final String TO_FIND_STRING = IFrameNode.DELIMITER_STRING;
+    private static final String CLOSE_BRACKET = "]";
+    public static final String REPLACE_STRING = "^";
+    public static final char REPLACE_CHAR = '^';
+    public static final String OPEN_STRING = "(";
+    public static final String SEPARATOR_STRING = REPLACE_STRING + OPEN_STRING;
+    public static final String CLOSE_STRING = ")";
+    public static final char CLOSE_CHAR = ')';
+    private static final Pattern PATTERN = Pattern.compile("!\\[[^\\]]*\\]\\([^)]+\\)");
+    @NonNull
+    public static IFrameProcessor create() {
+        return new IFrameProcessor();
+    }
+
+
+    @NonNull
+    public static String prepare(@NonNull String input) {
+        final StringBuilder builder = new StringBuilder(input);
+        if (builder.toString().toLowerCase().indexOf(TO_FIND_STRING) > -1) {
+            prepare(builder, TO_FIND_STRING);
+        }
+        return builder.toString();
+    }
+
+    public static void prepare(@NonNull StringBuilder builder, String finder) {
+
+        int start = builder.indexOf(finder);
+        int end;
+
+        while (start > -1) {
+
+            end = inLineDefinitionEnd(start + finder.length(), builder);
+            if (iconDefinitionValid(builder.subSequence(start, end))) {
+                int startEndBracket = builder.indexOf(CLOSE_BRACKET, start + 2);
+                if (checkingURLValidate(builder.substring(startEndBracket + 2, end - 1))) {
+                    builder.replace(start, start + 2, REPLACE_STRING);
+                    builder.replace(startEndBracket - 1, startEndBracket, REPLACE_STRING);
+                }
+            }
+            // move to next
+            start = builder.indexOf(finder, end);
+        }
+    }
+
+    private static int inLineDefinitionEnd(int startIndex, @NonNull StringBuilder builder) {
+
+        // all spaces, new lines, non-words or digits,
+
+        char c;
+
+        int end = -1;
+        for (int i = startIndex; i < builder.length(); i++) {
+            c = builder.charAt(i);
+            if (c == ')') {
+                end = i + 1;
+                break;
+            }
+        }
+
+        if (end == -1) {
+            end = builder.length();
+        }
+
+        return end;
+    }
+
+    private static boolean iconDefinitionValid(@NonNull CharSequence cs) {
+        final Matcher matcher = PATTERN.matcher(cs);
+        return matcher.matches();
+    }
+
+    private static boolean checkingURLValidate(@NonNull String cs) {
+        if (getYoutubeVideoId(cs) != null && !getYoutubeVideoId(cs).isEmpty()) {
+            return true;
+        }
+        if (getVimeoVideoId(cs) != null && !getVimeoVideoId(cs).isEmpty()) {
+            return true;
+        }
+        if (getDesmosId(cs) != null && !getDesmosId(cs).isEmpty()) {
+            return true;
+        }
+        return  false;
+    }
+
+    @Override
+    public char getOpeningCharacter() {
+        return REPLACE_CHAR;
+    }
+
+    @Override
+    public char getClosingCharacter() {
+        return CLOSE_CHAR;
+    }
+
+    @Override
+    public int getMinLength() {
+        return 1;
+    }
+
+    @Override
+    public int getDelimiterUse(DelimiterRun opener, DelimiterRun closer) {
+        return opener.length() >= 1 && closer.length() >= 1 ? 1 : 0;
+    }
+
+    @Override
+    public void process(Text opener, Text closer, int delimiterUse) {
+
+        final IFrameGroupNode iFrameGroupNode = new IFrameGroupNode();
+
+        final Node next = opener.getNext();
+
+        boolean handled = false;
+
+        // process only if we have exactly one Text node
+
+
+        final String text = ((Text) next).getLiteral();
+
+        if (!TextUtils.isEmpty(text)) {
+            // attempt to match
+            int start_link_index = text.indexOf(OPEN_STRING);
+            if (start_link_index >= 0) {
+                String link = text.substring(start_link_index + OPEN_STRING.length());
+                IFrameNode iFrameNode = new IFrameNode(link);
+                iFrameGroupNode.appendChild(iFrameNode);
+                next.unlink();
+                handled = true;
+            }
+        }
+
+        if (!handled) {
+            iFrameGroupNode.appendChild(new Text(REPLACE_STRING));
+            Node node;
+            for (Node tmp = opener.getNext(); tmp != null && tmp != closer; tmp = node) {
+                node = tmp.getNext();
+                // append a child anyway
+                iFrameGroupNode.appendChild(tmp);
+            }
+
+            iFrameGroupNode.appendChild(new Text(CLOSE_STRING));
+        }
+        opener.setLiteral("");
+        closer.setLiteral("");
+        opener.insertBefore(iFrameGroupNode);
+    }
+}
diff --git a/markwon-iframe-ext/src/main/java/io/noties/markwon/iframe/ext/IFrameUtils.java b/markwon-iframe-ext/src/main/java/io/noties/markwon/iframe/ext/IFrameUtils.java
new file mode 100644
index 00000000..93cf3a9b
--- /dev/null
+++ b/markwon-iframe-ext/src/main/java/io/noties/markwon/iframe/ext/IFrameUtils.java
@@ -0,0 +1,54 @@
+package io.noties.markwon.iframe.ext;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class IFrameUtils {
+    private final static String youtube_expression = "(?<=watch\\?v=|/videos/|embed\\/|youtu.be\\/|\\/v\\/|\\/e\\/|watch\\?v%3D|watch\\?feature=player_embedded&v=|%2Fvideos%2F|embed%\u200C\u200B2F|youtu.be%2F|%2Fv%2F)[^#\\&\\?\\n]*";
+    public static String getYoutubeVideoId(String videoUrl) {
+        if (videoUrl == null || videoUrl.trim().length() <= 0){
+            return null;
+        }
+        Pattern pattern = Pattern.compile(youtube_expression);
+        Matcher matcher = pattern.matcher(videoUrl);
+        try {
+            if (matcher.find())
+                return matcher.group();
+        } catch (ArrayIndexOutOfBoundsException ex) {
+            ex.printStackTrace();
+        }
+        return null;
+    }
+
+    private final static String vimeo_expression = "https?:\\/\\/(?:www\\.)?vimeo.com\\/(?:channels\\/(?:\\w+\\/)?|groups\\/([^\\/]*)\\/videos\\/|album\\/(\\d+)\\/video\\/|)(\\d+)(?:$|\\/|\\?)";
+    public static String getVimeoVideoId(String videoUrl) {
+        if (videoUrl == null || videoUrl.trim().length() <= 0){
+            return null;
+        }
+        Pattern pattern = Pattern.compile(vimeo_expression);
+        Matcher matcher = pattern.matcher(videoUrl);
+        try {
+            if (matcher.find())
+                return matcher.group(3);
+        } catch (ArrayIndexOutOfBoundsException ex) {
+            ex.printStackTrace();
+        }
+        return null;
+    }
+
+    private final static String desmos_expression = "https?:\\/\\/(www\\.)?desmos.com\\/calculator\\/(\\w+)($|\\/)";
+    public static String getDesmosId(String desmosURL) {
+        if (desmosURL == null || desmosURL.trim().length() <= 0){
+            return null;
+        }
+        Pattern pattern = Pattern.compile(desmos_expression);
+        Matcher matcher = pattern.matcher(desmosURL);
+        try {
+            if (matcher.find())
+                return matcher.group(2);
+        } catch (ArrayIndexOutOfBoundsException ex) {
+            ex.printStackTrace();
+        }
+        return null;
+    }
+}
diff --git a/markwon-iframe-ext/src/test/java/io/noties/markwon/iframe/ext/ExampleUnitTest.java b/markwon-iframe-ext/src/test/java/io/noties/markwon/iframe/ext/ExampleUnitTest.java
new file mode 100644
index 00000000..1548a00d
--- /dev/null
+++ b/markwon-iframe-ext/src/test/java/io/noties/markwon/iframe/ext/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package io.noties.markwon.iframe.ext;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+    @Test
+    public void addition_isCorrect() {
+        assertEquals(4, 2 + 2);
+    }
+}
\ No newline at end of file