Create inline plugin

This commit is contained in:
chengjunzhang61 2021-12-08 09:58:57 -05:00
parent da2276be67
commit 6006812f56
19 changed files with 752 additions and 0 deletions

1
markwon-ext-inline-latex/.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 project(':markwon-ext-latex')
implementation project(':markwon-span-ext')
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.ext.inlinelatex;
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.ext.inlinelatex.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.ext.inlinelatex">
</manifest>

View File

@ -0,0 +1,117 @@
package io.noties.markwon.ext.inlinelatex;
import androidx.annotation.NonNull;
import org.commonmark.internal.util.Parsing;
import org.commonmark.node.Block;
import org.commonmark.parser.block.AbstractBlockParser;
import org.commonmark.parser.block.AbstractBlockParserFactory;
import org.commonmark.parser.block.BlockContinue;
import org.commonmark.parser.block.BlockStart;
import org.commonmark.parser.block.MatchedBlockParser;
import org.commonmark.parser.block.ParserState;
import io.noties.markwon.ext.latex.JLatexMathBlock;
public class InLineLatexBlockParser extends AbstractBlockParser {
private static final char DOLLAR = '$';
private static final char SPACE = ' ';
private final JLatexMathBlock block = new JLatexMathBlock();
private final StringBuilder builder = new StringBuilder();
private final int signs;
InLineLatexBlockParser(int signs) {
this.signs = signs;
}
@Override
public Block getBlock() {
return block;
}
@Override
public BlockContinue tryContinue(ParserState parserState) {
final int nextNonSpaceIndex = parserState.getNextNonSpaceIndex();
final CharSequence line = parserState.getLine();
final int length = line.length();
// check for closing
if (parserState.getIndent() < Parsing.CODE_BLOCK_INDENT) {
if (consume(DOLLAR, line, nextNonSpaceIndex, length) == signs) {
// okay, we have our number of signs
// let's consume spaces until the end
if (Parsing.skip(SPACE, line, nextNonSpaceIndex + signs, length) == length) {
return BlockContinue.finished();
}
}
}
return BlockContinue.atIndex(parserState.getIndex());
}
@Override
public void addLine(CharSequence line) {
builder.append(line);
builder.append('\n');
}
@Override
public void closeBlock() {
block.latex(builder.toString());
}
public static class Factory extends AbstractBlockParserFactory {
@Override
public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
// let's define the spec:
// * 0-3 spaces before are allowed (Parsing.CODE_BLOCK_INDENT = 4)
// * 2+ subsequent `$` signs
// * any optional amount of spaces
// * new line
// * block is closed when the same amount of opening signs is met
final int indent = state.getIndent();
// check if it's an indented code block
if (indent >= Parsing.CODE_BLOCK_INDENT) {
return BlockStart.none();
}
final int nextNonSpaceIndex = state.getNextNonSpaceIndex();
final CharSequence line = state.getLine();
final int length = line.length();
final int signs = consume(DOLLAR, line, nextNonSpaceIndex, length);
// 2 is minimum
if (signs < 2) {
return BlockStart.none();
}
// consume spaces until the end of the line, if any other content is found -> NONE
if (Parsing.skip(SPACE, line, nextNonSpaceIndex + signs, length) != length) {
return BlockStart.none();
}
return BlockStart.of(new InLineLatexBlockParser(signs))
.atIndex(length + 1);
}
}
@SuppressWarnings("SameParameterValue")
private static int consume(char c, @NonNull CharSequence line, int start, int end) {
for (int i = start; i < end; i++) {
if (c != line.charAt(i)) {
return i - start;
}
}
// all consumed
return end - start;
}
}

View File

@ -0,0 +1,6 @@
package io.noties.markwon.ext.inlinelatex;
import org.commonmark.node.CustomNode;
public class InLineLatexGroupNode extends CustomNode {
}

View File

@ -0,0 +1,20 @@
package io.noties.markwon.ext.inlinelatex;
import androidx.annotation.NonNull;
import org.commonmark.node.CustomNode;
import org.commonmark.node.Delimited;
@SuppressWarnings("WeakerAccess")
public class InLineLatexNode extends CustomNode {
private String latex;
public String latex() {
return latex;
}
public void latex(String latex) {
this.latex = latex;
}
}

View File

@ -0,0 +1,126 @@
package io.noties.markwon.ext.inlinelatex;
import android.graphics.Color;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import androidx.annotation.NonNull;
import org.commonmark.parser.Parser;
import java.util.ArrayList;
import java.util.List;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
import io.noties.markwon.span.ext.CenteredImageSpan;
import ru.noties.jlatexmath.JLatexMathDrawable;
public class InLineLatexPlugIn extends AbstractMarkwonPlugin {
private static float mLatextSize;
private static int mScreenWidth;
@NonNull
public static InLineLatexPlugIn create(float latexSize, int screenWidth)
{
mLatextSize = latexSize;
mScreenWidth = screenWidth;
return new InLineLatexPlugIn();
}
@Override
public void configure(@NonNull Registry registry) {
registry.require(MarkwonInlineParserPlugin.class).factoryBuilder().addInlineProcessor(new InLineLatexProcessor());
}
@Override
public void configureParser(@NonNull Parser.Builder builder) {
builder.customBlockParserFactory(new InLineLatexBlockParser.Factory());
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.on(InLineLatexNode.class, new MarkwonVisitor.NodeVisitor<InLineLatexNode>() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull InLineLatexNode inLineLinkNode) {
final String latex = inLineLinkNode.latex();
int latxtColor = Color.BLACK;
int backgroundColor = Color.TRANSPARENT;
int errorColor = Color.parseColor("#ff3d00");
if (!TextUtils.isEmpty(latex)) {
if (latex.trim().equalsIgnoreCase("{")) {
String errTxt = "LaTeX syntax error";
ForegroundColorSpan fgColorSpan = new ForegroundColorSpan(errorColor);
visitor.builder().append(errTxt, fgColorSpan);
} else {
try {
int txtLength = latex.length();
float textWidth = mScreenWidth;
final JLatexMathDrawable latexDrawable = JLatexMathDrawable.builder(replaceLatexTag(latex))
.textSize(mLatextSize)
.color(latxtColor)
.background(backgroundColor)
.fitCanvas(false) // It will fix the truncated issue of inline latex
.build();
float latexWidth = latexDrawable.getIntrinsicWidth();
// Inline latex wrap
if (latexWidth > textWidth) {
float oneLetterWidth = latexWidth / txtLength;
if (oneLetterWidth < 22) {
oneLetterWidth = 22;
}
int allowLen = Math.round(textWidth / oneLetterWidth) - 2;
List<String> spiltedText = splitStringByLen(latex, allowLen);
for (int txtIndex = 0; txtIndex < spiltedText.size(); txtIndex ++) {
String subText = spiltedText.get(txtIndex);
int subTextLen = subText.length();
final JLatexMathDrawable subLatexDrawable = JLatexMathDrawable.builder(replaceLatexTag(subText))
.textSize(mLatextSize)
.color(latxtColor)
.background(backgroundColor)
.fitCanvas(true) // It will fix the truncated issue of inline latex
.build();
visitor.builder().append(subText, new CenteredImageSpan(subLatexDrawable));
}
} else {
visitor.builder().append(latex, new CenteredImageSpan(latexDrawable));
}
visitor.builder().append(' ');
} catch (Exception e) {
String errTxt = "LaTeX syntax error";
ForegroundColorSpan fgColorSpan = new ForegroundColorSpan(errorColor);
visitor.builder().append(errTxt, fgColorSpan);
}
}
}
}
});
}
@NonNull
@Override
public String processMarkdown(@NonNull String markdown) {
return InLineLatexProcessor.prepare(markdown);
}
public String replaceLatexTag(String latex) {
String latexText = latex.replaceAll("\\\\exist ", "\\\\exists ");
return latexText;
}
public List<String> splitStringByLen(String text, int length) {
List<String> strings = new ArrayList<String>();
int index = 0;
while (index < text.length()) {
String splitText = text.substring(index, Math.min(index + length,text.length()));
int nWhistSpace = splitText.lastIndexOf(" ");
if (nWhistSpace > 0 && splitText.length() >= length) {
String splitWord = splitText.substring(0, nWhistSpace);
strings.add(splitWord);
index += nWhistSpace;
} else {
strings.add(splitText);
index += length;
}
}
return strings;
}
}

View File

@ -0,0 +1,50 @@
package io.noties.markwon.ext.inlinelatex;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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.Pattern;
import io.noties.markwon.inlineparser.InlineProcessor;
public class InLineLatexProcessor extends InlineProcessor {
@NonNull
public static InLineLatexProcessor create() {
return new InLineLatexProcessor();
}
@NonNull
public static String prepare(@NonNull String input) {
final StringBuilder builder = new StringBuilder(input);
return builder.toString();
}
private static final Pattern RE = Pattern.compile("(\\${2})([\\s\\S]+?)\\1");
@Override
public char specialCharacter() {
return '$';
}
@Nullable
@Override
protected Node parse() {
final String latex = match(RE);
if (latex == null) {
return null;
}
final InLineLatexNode node = new InLineLatexNode();
node.latex(latex.substring(2, latex.length() - 2));
return node;
}
}

View File

@ -0,0 +1,17 @@
package io.noties.markwon.ext.inlinelatex;
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);
}
}

1
markwon-ext-inline/.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.ext.inline;
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.ext.inline.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.ext.inline">
</manifest>

View File

@ -0,0 +1,6 @@
package io.noties.markwon.ext.inline;
import org.commonmark.node.CustomNode;
public class InLineLinkGroupNode extends CustomNode {
}

View File

@ -0,0 +1,41 @@
package io.noties.markwon.ext.inline;
import androidx.annotation.NonNull;
import org.commonmark.node.CustomNode;
import org.commonmark.node.Delimited;
@SuppressWarnings("WeakerAccess")
public class InLineLinkNode extends CustomNode implements Delimited {
public static final String DELIMITER_STRING = "#";
private final String link;
public InLineLinkNode(@NonNull String link) {
this.link = link;
}
@NonNull
public String link() {
return link;
}
@Override
public String getOpeningDelimiter() {
return DELIMITER_STRING;
}
@Override
public String getClosingDelimiter() {
return DELIMITER_STRING;
}
@Override
public String toString() {
return "InLineLinkNode{" +
"link='" + link +'\'' + '\"' + '}';
}
}

View File

@ -0,0 +1,51 @@
package io.noties.markwon.ext.inline;
import android.text.TextUtils;
import android.view.View;
import androidx.annotation.NonNull;
import org.commonmark.parser.Parser;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.LinkResolver;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.core.spans.LinkSpan;
public class InLineLinkPlugIn extends AbstractMarkwonPlugin {
@NonNull
public static InLineLinkPlugIn create() {
return new InLineLinkPlugIn();
}
@Override
public void configureParser(@NonNull Parser.Builder builder) {
builder.customDelimiterProcessor(InLineLinkProcessor.create());
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.on(InLineLinkNode.class, new MarkwonVisitor.NodeVisitor<InLineLinkNode>() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull InLineLinkNode inLineLinkNode) {
final String link = inLineLinkNode.link();
if (!TextUtils.isEmpty(link)) {
final int length = visitor.length();
visitor.builder().append(link);
visitor.setSpans(length, new LinkSpan(visitor.configuration().theme(), link, new LinkResolver() {
@Override
public void resolve(@NonNull View view, @NonNull String link) {
}
}));
visitor.builder().append(' ');
}
}
});
}
@NonNull
@Override
public String processMarkdown(@NonNull String markdown) {
return InLineLinkProcessor.prepare(markdown);
}
}

View File

@ -0,0 +1,139 @@
package io.noties.markwon.ext.inline;
import android.text.TextUtils;
import android.util.Patterns;
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 InLineLinkProcessor implements DelimiterProcessor {
private static final String TO_FIND = InLineLinkNode.DELIMITER_STRING;
private static final Pattern PATTERN = Pattern.compile("#[0-9]{1,10}");
@NonNull
public static InLineLinkProcessor create() {
return new InLineLinkProcessor();
}
@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 = inLineDefinitionEnd(start + TO_FIND.length(), builder);
if (iconDefinitionValid(builder.subSequence(start, end))) {
builder.insert(end, '#');
}
// move to next
start = builder.indexOf(TO_FIND, end);
}
}
private static int inLineDefinitionEnd(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.isDigit(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();
}
@Override
public char getOpeningCharacter() {
return '#';
}
@Override
public char getClosingCharacter() {
return '#';
}
@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 InLineLinkGroupNode inLineLinkGroupNode = new InLineLinkGroupNode();
final Node next = opener.getNext();
boolean handled = false;
// process only if we have exactly one Text node
if (next instanceof Text) {
final String text = ((Text) next).getLiteral();
if (!TextUtils.isEmpty(text)) {
// attempt to match
InLineLinkNode iconNode = new InLineLinkNode(InLineLinkNode.DELIMITER_STRING + text);
inLineLinkGroupNode.appendChild(iconNode);
next.unlink();
handled = true;
}
}
if (!handled) {
// restore delimiters if we didn't match
inLineLinkGroupNode.appendChild(new Text(InLineLinkNode.DELIMITER_STRING));
Node node;
for (Node tmp = opener.getNext(); tmp != null && tmp != closer; tmp = node) {
node = tmp.getNext();
// append a child anyway
inLineLinkGroupNode.appendChild(tmp);
}
inLineLinkGroupNode.appendChild(new Text(InLineLinkNode.DELIMITER_STRING));
}
opener.insertBefore(inLineLinkGroupNode);
}
}

View File

@ -0,0 +1,17 @@
package io.noties.markwon.ext.inline;
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);
}
}