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 Testing documentation
+ */
+@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 @@
+
+
+
+
\ 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() {
+ @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 Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file