Create emoji plugin

This commit is contained in:
chengjunzhang61 2021-12-08 09:57:37 -05:00
parent 88d56ed627
commit da2276be67
3088 changed files with 27382 additions and 0 deletions

1
markwon-emoji/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,50 @@
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')
implementation 'com.google.code.gson:gson:2.8.6'
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.emoji.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.emoji.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.emoji.ext">
</manifest>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,55 @@
package io.noties.markwon.emoji.ext;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.style.ImageSpan;
import java.lang.ref.WeakReference;
public class CenterSpan extends ImageSpan {
private WeakReference<Drawable> mDrawableRef;
public CenterSpan(Context context, final int drawableRes) {
super(context, drawableRes);
}
public CenterSpan(Drawable drawable) {
super(drawable);
}
@Override
public int getSize(Paint paint, CharSequence text, int start, int end,
Paint.FontMetricsInt fontMetricsInt) {
Drawable drawable = getDrawable();
Rect rect = drawable.getBounds();
if (fontMetricsInt != null) {
Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
int fontHeight = fmPaint.descent - fmPaint.ascent;
int drHeight = rect.bottom - rect.top;
int centerY = fmPaint.ascent + fontHeight / 2;
fontMetricsInt.ascent = centerY - drHeight / 2;
fontMetricsInt.top = fontMetricsInt.ascent;
fontMetricsInt.bottom = centerY + drHeight / 2;
fontMetricsInt.descent = fontMetricsInt.bottom;
}
return rect.right;
}
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end,
float x, int top, int y, int bottom, Paint paint) {
Drawable drawable = getDrawable();
canvas.save();
Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt();
int fontHeight = fmPaint.descent - fmPaint.ascent;
int centerY = y + fmPaint.descent - fontHeight / 2;
int transY = centerY - (drawable.getBounds().bottom - drawable.getBounds().top) / 2;
canvas.translate(x, transY);
drawable.draw(canvas);
canvas.restore();
}
}

View File

@ -0,0 +1,62 @@
package io.noties.markwon.emoji.ext;
import java.util.List;
public class EmojiCode {
private String unicode;
private String unicode_alternates;
private String name;
private String shortname;
private String category;
private String emoji_order;
private List<Object> aliases;
private List<Object> aliases_ascii;
private List<String> keywords;
public enum EMOJI_SIZE{
SMALL, MEDIDUM, LARGE;
}
public String getImageName(){
String code = unicode.replace("-", "_");
code = "tw_" + code;
return code;
}
public String getUnicode() {
return unicode;
}
public String getUnicode_alternates() {
return unicode_alternates;
}
public String getName() {
return name;
}
public String getShortname() {
return shortname;
}
public String getCategory() {
return category;
}
public String getEmoji_order() {
return emoji_order;
}
public List<Object> getAliases() {
return aliases;
}
public List<Object> getAliases_ascii() {
return aliases_ascii;
}
public List<String> getKeywords() {
return keywords;
}
}

View File

@ -0,0 +1,8 @@
package io.noties.markwon.emoji.ext;
import org.commonmark.node.CustomNode;
@SuppressWarnings("WeakerAccess")
public class EmojiGroupNode extends CustomNode {
}

View File

@ -0,0 +1,37 @@
package io.noties.markwon.emoji.ext;
import androidx.annotation.NonNull;
import org.commonmark.node.CustomNode;
import org.commonmark.node.Delimited;
@SuppressWarnings("WeakerAccess")
public class EmojiNode extends CustomNode implements Delimited {
public static final char DELIMITER = ':';
public static final String DELIMITER_STRING = "" + DELIMITER;
private final String colonCode;
public EmojiNode(@NonNull String colonCode) {
this.colonCode = colonCode;
}
@NonNull
public String getColonCode() {
return colonCode;
}
@Override
public String getOpeningDelimiter() {
return DELIMITER_STRING;
}
@Override
public String getClosingDelimiter() {
return DELIMITER_STRING;
}
}

View File

@ -0,0 +1,53 @@
package io.noties.markwon.emoji.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 EmojiPlugin extends AbstractMarkwonPlugin {
@NonNull
public static EmojiPlugin create(@NonNull EmojiSpanProvider emojiSpanProvider) {
return new EmojiPlugin(emojiSpanProvider);
}
private final EmojiSpanProvider emojiSpanProvider;
EmojiPlugin(@NonNull EmojiSpanProvider emojiSpanProvider) {
this.emojiSpanProvider = emojiSpanProvider;
}
@Override
public void configureParser(@NonNull Parser.Builder builder) {
builder.customDelimiterProcessor(EmojiProcessor.create());
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.on(EmojiNode.class, new MarkwonVisitor.NodeVisitor<EmojiNode>() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull EmojiNode emojiNode) {
final String colonCode = emojiNode.getColonCode();
if (!TextUtils.isEmpty(colonCode)) {
final int length = visitor.length();
visitor.builder().append(colonCode);
visitor.setSpans(length, emojiSpanProvider.provide(colonCode));
visitor.builder().append(' ');
}
}
});
}
@NonNull
@Override
public String processMarkdown(@NonNull String markdown) {
return EmojiProcessor.prepare(markdown);
}
}

View File

@ -0,0 +1,158 @@
package io.noties.markwon.emoji.ext;
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;
@SuppressWarnings("WeakerAccess")
public class EmojiProcessor implements DelimiterProcessor {
@NonNull
public static EmojiProcessor create() {
return new EmojiProcessor();
}
// ic-home-black-24
private static final Pattern PATTERN = Pattern.compile("[a-z]{2,50}|[a-z]{2,50}[_][a-z]{2,50}|[a-z]{2,50}[_][a-z]{2,50}[_][a-z]{2,50}]");
private static final String TO_FIND = EmojiNode.DELIMITER_STRING;
/**
* Should be used when input string does not wrap icon definition with `@` from both ends.
* So, `@ic-home-white-24` would become `@ic-home-white-24@`. This way parsing is easier
* and more predictable (cannot specify multiple ending delimiters, as we would require them:
* space, newline, end of a document, and a lot of more)
*
* @param input to process
* @return processed string
* @see #prepare(StringBuilder)
*/
@NonNull
public static String prepare(@NonNull String input) {
final StringBuilder builder = new StringBuilder(input);
prepare(builder);
return builder.toString();
}
public static void prepare(@NonNull StringBuilder builder) {
// int start = builder.indexOf(TO_FIND);
// int end;
//
// while (start > -1) {
//
// end = iconDefinitionEnd(start + TO_FIND.length(), builder);
//
// // if we match our pattern, append `@` else ignore
// if (iconDefinitionValid(builder.subSequence(start + 1, end))) {
// builder.insert(end, '@');
// }
//
// // move to next
// start = builder.indexOf(TO_FIND, end);
// }
}
@Override
public char getOpeningCharacter() {
return EmojiNode.DELIMITER;
}
@Override
public char getClosingCharacter() {
return EmojiNode.DELIMITER;
}
@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 EmojiGroupNode emojiGroupNode = new EmojiGroupNode();
final Node next = opener.getNext();
boolean handled = false;
// process only if we have exactly one Text node
if (next instanceof Text && next.getNext() == closer) {
final String text = ((Text) next).getLiteral();
if (!TextUtils.isEmpty(text)) {
// attempt to match
final Matcher matcher = PATTERN.matcher(text);
if (matcher.matches()) {
final EmojiNode emojiNode = new EmojiNode(
text
);
emojiGroupNode.appendChild(emojiNode);
next.unlink();
handled = true;
}
}
}
if (!handled) {
// restore delimiters if we didn't match
emojiGroupNode.appendChild(new Text(EmojiNode.DELIMITER_STRING));
Node node;
for (Node tmp = opener.getNext(); tmp != null && tmp != closer; tmp = node) {
node = tmp.getNext();
// append a child anyway
emojiGroupNode.appendChild(tmp);
}
emojiGroupNode.appendChild(new Text(EmojiNode.DELIMITER_STRING));
}
opener.insertBefore(emojiGroupNode);
}
private static int iconDefinitionEnd(int index, @NonNull StringBuilder builder) {
// all spaces, new lines, non-words or digits,
char c;
int end = -1;
for (int i = index; i < builder.length(); i++) {
c = builder.charAt(i);
if (Character.isWhitespace(c)
|| !(Character.isLetterOrDigit(c) || c == '-' || c == '_')) {
end = i;
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();
}
}

View File

@ -0,0 +1,91 @@
package io.noties.markwon.emoji.ext;
import android.content.Context;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.util.Map;
@SuppressWarnings("WeakerAccess")
public abstract class EmojiSpanProvider {
@SuppressWarnings("SameParameterValue")
@NonNull
public static EmojiSpanProvider create(@NonNull Context context, float textSize) {
return new Impl(context, textSize);
}
@NonNull
public abstract Object provide(@NonNull String text);
private static class Impl extends EmojiSpanProvider {
private final Context context;
private float textSize;
private Map<String, EmojiCode> emojiMap;
Impl(@NonNull Context context, float textSize) {
this.context = context;
this.textSize = textSize;
this.emojiMap = loadEmojiMapFromAssets();
}
private Map<String, EmojiCode> loadEmojiMapFromAssets(){
try{
InputStream is = this.context.getAssets().open("emoji.json");
int size = is.available();
byte[] buffer = new byte[size];
is.read(buffer);
is.close();
String jsonString = new String(buffer, "UTF-8");
Gson gson = new Gson();
Type mapType = new TypeToken<Map<String, EmojiCode>>(){}.getType();
Map<String, EmojiCode> map = gson.fromJson(jsonString, mapType);
return map;
}catch (IOException e){
e.printStackTrace();
return null;
}
}
@NonNull
@Override
public Object provide(@NonNull String colonCode) {
return new CenterSpan(getDrawable(colonCode));
}
@NonNull
private Drawable getDrawable(String colonCode) {
EmojiCode emojiCode = emojiMap.get(colonCode);
int drawbleId = -1;
if (emojiCode != null){
String imageName = emojiCode.getImageName();
try{
drawbleId = context.getResources().getIdentifier(imageName, "drawable", context.getPackageName());
}catch (Exception e){
e.printStackTrace();
}
}
Drawable emojiDrawable;
if (drawbleId != -1) {
emojiDrawable = context.getResources().getDrawable(drawbleId);
} else {
emojiDrawable = context.getResources().getDrawable(R.drawable.tw_1f30d);
}
int emojiHeight = emojiDrawable.getIntrinsicHeight();
int emojiWidth = emojiDrawable.getIntrinsicWidth();
int requiredWidth = emojiWidth * (int)textSize / emojiHeight;
emojiDrawable.setBounds(0, 0, requiredWidth, (int) textSize);
return emojiDrawable;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 697 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 989 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 662 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 481 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 847 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 835 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 459 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 978 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 919 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 871 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 805 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1000 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 669 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 356 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 961 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 913 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 696 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 B

Some files were not shown because too many files have changed in this diff Show More