Create iframe plugin

This commit is contained in:
chengjunzhang61 2021-12-08 10:00:03 -05:00
parent c928750ff0
commit 50d331bc87
10 changed files with 387 additions and 0 deletions

1
markwon-iframe-ext/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -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']
}
}

View File

@ -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());
}
}

View File

@ -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>

View File

@ -0,0 +1,7 @@
package io.noties.markwon.iframe.ext;
import org.commonmark.node.CustomNode;
public class IFrameGroupNode extends CustomNode {
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}