Create inline plugin
This commit is contained in:
parent
da2276be67
commit
6006812f56
1
markwon-ext-inline-latex/.gitignore
vendored
Normal file
1
markwon-ext-inline-latex/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
50
markwon-ext-inline-latex/build.gradle
Normal file
50
markwon-ext-inline-latex/build.gradle
Normal 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']
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
5
markwon-ext-inline-latex/src/main/AndroidManifest.xml
Normal file
5
markwon-ext-inline-latex/src/main/AndroidManifest.xml
Normal 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>
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package io.noties.markwon.ext.inlinelatex;
|
||||||
|
|
||||||
|
import org.commonmark.node.CustomNode;
|
||||||
|
|
||||||
|
public class InLineLatexGroupNode extends CustomNode {
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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
1
markwon-ext-inline/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/build
|
48
markwon-ext-inline/build.gradle
Normal file
48
markwon-ext-inline/build.gradle
Normal 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']
|
||||||
|
}
|
||||||
|
}
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
5
markwon-ext-inline/src/main/AndroidManifest.xml
Normal file
5
markwon-ext-inline/src/main/AndroidManifest.xml
Normal 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>
|
@ -0,0 +1,6 @@
|
|||||||
|
package io.noties.markwon.ext.inline;
|
||||||
|
|
||||||
|
import org.commonmark.node.CustomNode;
|
||||||
|
|
||||||
|
public class InLineLinkGroupNode extends CustomNode {
|
||||||
|
}
|
@ -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 +'\'' + '\"' + '}';
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user