Working with latex plugin

This commit is contained in:
Dimitry Ivanov 2020-02-14 18:35:44 +03:00
parent d78b278b86
commit 7af0ead3a3
5 changed files with 332 additions and 149 deletions

View File

@ -4,6 +4,8 @@
[![Build](https://github.com/noties/Markwon/workflows/Build/badge.svg)](https://github.com/noties/Markwon/actions) [![Build](https://github.com/noties/Markwon/workflows/Build/badge.svg)](https://github.com/noties/Markwon/actions)
![hey](http://img.xiaoyv.top/bbs/201603246-20aa1b8ad8bf27df3c906473619c2d84.jpg)
**Markwon** is a markdown library for Android. It parses markdown **Markwon** is a markdown library for Android. It parses markdown
following [commonmark-spec] with the help of amazing [commonmark-java] following [commonmark-spec] with the help of amazing [commonmark-java]
library and renders result as _Android-native_ Spannables. **No HTML** library and renders result as _Android-native_ Spannables. **No HTML**

View File

@ -1,5 +1,9 @@
package io.noties.markwon.ext.latex; package io.noties.markwon.ext.latex;
import android.util.Log;
import androidx.annotation.NonNull;
import org.commonmark.internal.util.Parsing; import org.commonmark.internal.util.Parsing;
import org.commonmark.node.Block; import org.commonmark.node.Block;
import org.commonmark.parser.block.AbstractBlockParser; import org.commonmark.parser.block.AbstractBlockParser;
@ -11,11 +15,21 @@ import org.commonmark.parser.block.ParserState;
public class JLatexMathBlockParser extends AbstractBlockParser { public class JLatexMathBlockParser extends AbstractBlockParser {
private static final char DOLLAR = '$';
private static final char SPACE = ' ';
private final JLatexMathBlock block = new JLatexMathBlock(); private final JLatexMathBlock block = new JLatexMathBlock();
private final StringBuilder builder = new StringBuilder(); private final StringBuilder builder = new StringBuilder();
private boolean isClosed; // private boolean isClosed;
private final int signs;
@SuppressWarnings("WeakerAccess")
JLatexMathBlockParser(int signs) {
this.signs = signs;
}
@Override @Override
public Block getBlock() { public Block getBlock() {
@ -24,9 +38,22 @@ public class JLatexMathBlockParser extends AbstractBlockParser {
@Override @Override
public BlockContinue tryContinue(ParserState parserState) { public BlockContinue tryContinue(ParserState parserState) {
final int nextNonSpaceIndex = parserState.getNextNonSpaceIndex();
final CharSequence line = parserState.getLine();
final int length = line.length();
if (isClosed) { // check for closing
return BlockContinue.finished(); if (parserState.getIndent() < Parsing.CODE_BLOCK_INDENT) {
Log.e("LTX", String.format("signs: %d, skip dollar: %s", signs, Parsing.skip(DOLLAR, line, nextNonSpaceIndex, length)));
// if (Parsing.skip(DOLLAR, line, nextNonSpaceIndex, length) == signs) {
if (consume(DOLLAR, line, nextNonSpaceIndex, length) == signs) {
// okay, we have our number of signs
// let's consume spaces until the end
Log.e("LTX", String.format("length; %d, skip spaces: %s", length, Parsing.skip(SPACE, line, nextNonSpaceIndex + signs, length)));
if (Parsing.skip(SPACE, line, nextNonSpaceIndex + signs, length) == length) {
return BlockContinue.finished();
}
}
} }
return BlockContinue.atIndex(parserState.getIndex()); return BlockContinue.atIndex(parserState.getIndex());
@ -34,21 +61,24 @@ public class JLatexMathBlockParser extends AbstractBlockParser {
@Override @Override
public void addLine(CharSequence line) { public void addLine(CharSequence line) {
//
if (builder.length() > 0) { // if (builder.length() > 0) {
builder.append('\n'); // builder.append('\n');
} // }
//
// builder.append(line);
//
// final int length = builder.length();
// if (length > 1) {
// isClosed = '$' == builder.charAt(length - 1)
// && '$' == builder.charAt(length - 2);
// if (isClosed) {
// builder.replace(length - 2, length, "");
// }
// }
Log.e("LTX", "addLine: " + line);
builder.append(line); builder.append(line);
builder.append('\n');
final int length = builder.length();
if (length > 1) {
isClosed = '$' == builder.charAt(length - 1)
&& '$' == builder.charAt(length - 2);
if (isClosed) {
builder.replace(length - 2, length, "");
}
}
} }
@Override @Override
@ -58,37 +88,111 @@ public class JLatexMathBlockParser extends AbstractBlockParser {
public static class Factory extends AbstractBlockParserFactory { public static class Factory extends AbstractBlockParserFactory {
// private static final Pattern RE = Pattern.compile("(\\${2,}) *$");
@Override @Override
public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) { 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(); final int indent = state.getIndent();
// check if it's an indented code block // check if it's an indented code block
if (indent < Parsing.CODE_BLOCK_INDENT) { if (indent >= Parsing.CODE_BLOCK_INDENT) {
final int nextNonSpaceIndex = state.getNextNonSpaceIndex(); return BlockStart.none();
final CharSequence line = state.getLine();
final int length = line.length();
// we are looking for 2 `$$` subsequent signs
// and immediate new-line or arbitrary number of white spaces (we check for the first one)
// so, nextNonSpaceIndex + 2 >= length and both symbols are `$`s
final int diff = length - (nextNonSpaceIndex + 2);
if (diff >= 0) {
// check for both `$`
if (line.charAt(nextNonSpaceIndex) == '$'
&& line.charAt(nextNonSpaceIndex + 1) == '$') {
if (diff > 0) {
if (!Character.isWhitespace(line.charAt(nextNonSpaceIndex + 2))) {
return BlockStart.none();
}
// consume all until new-line or first not-white-space char
}
}
}
} }
return BlockStart.none(); final int nextNonSpaceIndex = state.getNextNonSpaceIndex();
final CharSequence line = state.getLine();
final int length = line.length();
// final int signs = Parsing.skip(DOLLAR, line, nextNonSpaceIndex, length) - 1;
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();
}
Log.e("LTX", String.format("signs: %s, next: %d, length: %d, line: '%s'", signs, nextNonSpaceIndex, length, line));
return BlockStart.of(new JLatexMathBlockParser(signs))
.atIndex(length + 1);
// // check if it's an indented code block
// if (indent < Parsing.CODE_BLOCK_INDENT) {
//
// final int nextNonSpaceIndex = state.getNextNonSpaceIndex();
// final CharSequence line = state.getLine();
// final int length = line.length();
//
// final int signs = Parsing.skip('$', 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(' ', line, nextNonSpaceIndex + signs, length) != length) {
// return BlockStart.none();
// }
//
//// // consume spaces until the end of the line, if any other content is found -> NONE
//// if ((nextNonSpaceIndex + signs) < length) {
//// // check if more content is available
//// if (Parsing.skip(' ', line,nextNonSpaceIndex + signs, length) != length) {
//// return BlockStart.none();
//// }
//// }
//
//// final Matcher matcher = RE.matcher(line);
//// matcher.region(nextNonSpaceIndex, length);
//
//// Log.e("LATEX", String.format("nonSpace: %d, length: %s, line: '%s'", nextNonSpaceIndex, length, line));
//
// // we are looking for 2 `$$` subsequent signs
// // and immediate new-line or arbitrary number of white spaces (we check for the first one)
// // so, nextNonSpaceIndex + 2 >= length and both symbols are `$`s
// final int diff = length - (nextNonSpaceIndex + 2);
// if (diff >= 0) {
// // check for both `$`
// if (line.charAt(nextNonSpaceIndex) == '$'
// && line.charAt(nextNonSpaceIndex + 1) == '$') {
//
// if (diff > 0) {
// if (!Character.isWhitespace(line.charAt(nextNonSpaceIndex + 2))) {
// return BlockStart.none();
// }
// return BlockStart.of(new JLatexMathBlockParser()).atIndex(nextNonSpaceIndex + 3);
// }
//
// }
// }
// }
//
// return BlockStart.none();
} }
} }
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

@ -1,5 +1,6 @@
package io.noties.markwon.ext.latex; package io.noties.markwon.ext.latex;
import android.graphics.Paint;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Handler; import android.os.Handler;
@ -9,6 +10,7 @@ import android.text.Spanned;
import android.util.Log; import android.util.Log;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.Px; import androidx.annotation.Px;
@ -26,10 +28,12 @@ import java.util.concurrent.Future;
import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.MarkwonVisitor; import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.image.AsyncDrawable; import io.noties.markwon.image.AsyncDrawable;
import io.noties.markwon.image.AsyncDrawableLoader; import io.noties.markwon.image.AsyncDrawableLoader;
import io.noties.markwon.image.AsyncDrawableScheduler; import io.noties.markwon.image.AsyncDrawableScheduler;
import io.noties.markwon.image.AsyncDrawableSpan; import io.noties.markwon.image.AsyncDrawableSpan;
import io.noties.markwon.image.ImageSize;
import io.noties.markwon.image.ImageSizeResolver; import io.noties.markwon.image.ImageSizeResolver;
import io.noties.markwon.image.ImageSizeResolverDef; import io.noties.markwon.image.ImageSizeResolverDef;
import io.noties.markwon.inlineparser.MarkwonInlineParser; import io.noties.markwon.inlineparser.MarkwonInlineParser;
@ -129,7 +133,8 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
// if it's $$\n -> block // if it's $$\n -> block
// if it's $$\\dhdsfjh$$ -> inline // if it's $$\\dhdsfjh$$ -> inline
// builder.customBlockParserFactory(new JLatexMathBlockParser.Factory()); builder.customBlockParserFactory(new JLatexMathBlockParser.Factory());
final InlineParserFactory factory = MarkwonInlineParser.factoryBuilder() final InlineParserFactory factory = MarkwonInlineParser.factoryBuilder()
.addInlineProcessor(new JLatexMathInlineProcessor()) .addInlineProcessor(new JLatexMathInlineProcessor())
.build(); .build();
@ -142,6 +147,8 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
@Override @Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull JLatexMathBlock jLatexMathBlock) { public void visit(@NonNull MarkwonVisitor visitor, @NonNull JLatexMathBlock jLatexMathBlock) {
visitor.ensureNewLine();
final String latex = jLatexMathBlock.latex(); final String latex = jLatexMathBlock.latex();
final int length = visitor.length(); final int length = visitor.length();
@ -155,15 +162,21 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
final AsyncDrawableSpan span = new AsyncDrawableSpan( final AsyncDrawableSpan span = new AsyncDrawableSpan(
configuration.theme(), configuration.theme(),
new AsyncDrawable( new JLatextAsyncDrawable(
latex, latex,
jLatextAsyncDrawableLoader, jLatextAsyncDrawableLoader,
jLatexImageSizeResolver, jLatexImageSizeResolver,
null), null,
true),
AsyncDrawableSpan.ALIGN_CENTER, AsyncDrawableSpan.ALIGN_CENTER,
false); false);
visitor.setSpans(length, span); visitor.setSpans(length, span);
if (visitor.hasNext(jLatexMathBlock)) {
visitor.ensureNewLine();
visitor.forceNewLine();
}
} }
}); });
builder.on(JLatexMathNode.class, new MarkwonVisitor.NodeVisitor<JLatexMathNode>() { builder.on(JLatexMathNode.class, new MarkwonVisitor.NodeVisitor<JLatexMathNode>() {
@ -180,13 +193,14 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
final MarkwonConfiguration configuration = visitor.configuration(); final MarkwonConfiguration configuration = visitor.configuration();
final AsyncDrawableSpan span = new AsyncDrawableSpan( final AsyncDrawableSpan span = new JLatexAsyncDrawableSpan(
configuration.theme(), configuration.theme(),
new AsyncDrawable( new JLatextAsyncDrawable(
latex, latex,
jLatextAsyncDrawableLoader, jLatextAsyncDrawableLoader,
new ImageSizeResolverDef(), new ImageSizeResolverDef(),
null), null,
false),
AsyncDrawableSpan.ALIGN_CENTER, AsyncDrawableSpan.ALIGN_CENTER,
false); false);
@ -195,77 +209,6 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
}); });
} }
// private static class LatexInlineProcessor extends InlineProcessor {
//
// @Override
// public char specialCharacter() {
// return '$';
// }
//
// @Nullable
// @Override
// protected Node parse() {
//
// final int start = index;
//
// index += 1;
// if (peek() != '$') {
// index = start;
// return null;
// }
//
// // must be not $
// index += 1;
// if (peek() == '$') {
// return text("$");
// }
//
// // find next '$$', but not broken with 2(or more) new lines
//
// boolean dollar = false;
// boolean newLine = false;
// boolean found = false;
//
// index += 1;
// final int length = input.length();
//
// while (index < length) {
// final char c = peek();
// if (c == '\n') {
// if (newLine) {
// // second new line
// break;
// }
// newLine = true;
// dollar = false; // cannot be on another line
// } else {
// newLine = false;
// if (c == '$') {
// if (dollar) {
// found = true;
// // advance
// index += 1;
// break;
// }
// dollar = true;
// } else {
// dollar = false;
// }
// }
// index += 1;
// }
//
// if (found) {
// final JLatexMathBlock block = new JLatexMathBlock();
// block.latex(input.substring(start + 2, index - 2));
// index += 1;
// return block;
// }
//
// return null;
// }
// }
@Override @Override
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
AsyncDrawableScheduler.unschedule(textView); AsyncDrawableScheduler.unschedule(textView);
@ -401,20 +344,53 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
// @since 4.0.1 (background provider can be null) // @since 4.0.1 (background provider can be null)
final BackgroundProvider backgroundProvider = config.backgroundProvider; final BackgroundProvider backgroundProvider = config.backgroundProvider;
final JLatexMathDrawable jLatexMathDrawable;
final JLatextAsyncDrawable jLatextAsyncDrawable = (JLatextAsyncDrawable) drawable;
if (jLatextAsyncDrawable.isBlock) {
// create JLatexMathDrawable
//noinspection ConstantConditions
jLatexMathDrawable =
JLatexMathDrawable.builder(drawable.getDestination())
.textSize(config.textSize)
.background(backgroundProvider != null ? backgroundProvider.provide() : null)
.align(config.align)
.fitCanvas(config.fitCanvas)
.padding(
config.paddingHorizontal,
config.paddingVertical,
config.paddingHorizontal,
config.paddingVertical)
.build();
} else {
jLatexMathDrawable =
JLatexMathDrawable.builder(drawable.getDestination())
.textSize(config.textSize)
// .background(backgroundProvider != null ? backgroundProvider.provide() : null)
// .align(config.align)
// .fitCanvas(config.fitCanvas)
// .padding(
// config.paddingHorizontal,
// config.paddingVertical,
// config.paddingHorizontal,
// config.paddingVertical)
.build();
}
// create JLatexMathDrawable // create JLatexMathDrawable
//noinspection ConstantConditions // //noinspection ConstantConditions
final JLatexMathDrawable jLatexMathDrawable = // final JLatexMathDrawable jLatexMathDrawable =
JLatexMathDrawable.builder(drawable.getDestination()) // JLatexMathDrawable.builder(drawable.getDestination())
.textSize(config.textSize) // .textSize(config.textSize)
.background(backgroundProvider != null ? backgroundProvider.provide() : null) // .background(backgroundProvider != null ? backgroundProvider.provide() : null)
.align(config.align) // .align(config.align)
.fitCanvas(false /*config.fitCanvas*/) // .fitCanvas(config.fitCanvas)
.padding( // .padding(
config.paddingHorizontal, // config.paddingHorizontal,
config.paddingVertical, // config.paddingVertical,
config.paddingHorizontal, // config.paddingHorizontal,
config.paddingVertical) // config.paddingVertical)
.build(); // .build();
// we must post to handler, but also have a way to identify the drawable // we must post to handler, but also have a way to identify the drawable
// for which we are posting (in case of cancellation) // for which we are posting (in case of cancellation)
@ -496,4 +472,66 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
return imageBounds; return imageBounds;
} }
} }
private static class JLatextAsyncDrawable extends AsyncDrawable {
private final boolean isBlock;
public JLatextAsyncDrawable(
@NonNull String destination,
@NonNull AsyncDrawableLoader loader,
@NonNull ImageSizeResolver imageSizeResolver,
@Nullable ImageSize imageSize,
boolean isBlock
) {
super(destination, loader, imageSizeResolver, imageSize);
this.isBlock = isBlock;
}
}
private static class JLatexAsyncDrawableSpan extends AsyncDrawableSpan {
private final AsyncDrawable drawable;
public JLatexAsyncDrawableSpan(@NonNull MarkwonTheme theme, @NonNull AsyncDrawable drawable, int alignment, boolean replacementTextIsLink) {
super(theme, drawable, alignment, replacementTextIsLink);
this.drawable = drawable;
}
@Override
public int getSize(
@NonNull Paint paint,
CharSequence text,
@IntRange(from = 0) int start,
@IntRange(from = 0) int end,
@Nullable Paint.FontMetricsInt fm) {
// if we have no async drawable result - we will just render text
final int size;
if (drawable.hasResult()) {
final Rect rect = drawable.getBounds();
if (fm != null) {
final int half = rect.bottom / 2;
fm.ascent = -half;
fm.descent = half;
fm.top = fm.ascent;
fm.bottom = 0;
}
size = rect.right;
} else {
// NB, no specific text handling (no new lines, etc)
size = (int) (paint.measureText(text, start, end) + .5F);
}
return size;
}
}
} }

View File

@ -0,0 +1,28 @@
package io.noties.markwon.ext.latex;
import android.graphics.Rect;
/**
* @since 4.3.0-SNAPSHOT
*/
public class JLatexMathTheme {
private float textSize;
private float inlineTextSize;
private float blockTextSize;
// TODO: move to a class
private JLatexMathPlugin.BackgroundProvider backgroundProvider;
private JLatexMathPlugin.BackgroundProvider inlineBackgroundProvider;
private JLatexMathPlugin.BackgroundProvider blockBackgroundProvider;
private boolean blockFitCanvas;
// horizontal alignment (when there is additional horizontal space)
private int blockAlign;
private Rect padding;
private Rect inlinePadding;
private Rect blockPadding;
}

View File

@ -4,14 +4,19 @@ import android.app.Activity;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.commonmark.node.Node;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon; import io.noties.markwon.Markwon;
import io.noties.markwon.ext.latex.JLatexMathPlugin; import io.noties.markwon.ext.latex.JLatexMathPlugin;
import io.noties.markwon.sample.R; import io.noties.markwon.sample.R;
import io.noties.markwon.utils.DumpNodes;
import ru.noties.jlatexmath.JLatexMathDrawable; import ru.noties.jlatexmath.JLatexMathDrawable;
public class LatexActivity extends Activity { public class LatexActivity extends Activity {
@ -44,27 +49,33 @@ public class LatexActivity extends Activity {
// latex += "\\end{array}"; // latex += "\\end{array}";
final String markdown = "# Example of LaTeX\n\nhello there: $$" final String markdown = "# Example of LaTeX\n\nhello there: $$"
+ latex + "$$\n\n something like **this**"; + latex + "$$ so nice, really?\n\n $$ \n" + latex + "\n$$\n\n $$ \n" + latex + "\n$$";
final Markwon markwon = Markwon.builder(this) final Markwon markwon = Markwon.builder(this)
// .usePlugin(JLatexMathPlugin.create(textView.getTextSize(), new JLatexMathPlugin.BuilderConfigure() { .usePlugin(JLatexMathPlugin.create(textView.getTextSize(), new JLatexMathPlugin.BuilderConfigure() {
// @Override @Override
// public void configureBuilder(@NonNull JLatexMathPlugin.Builder builder) { public void configureBuilder(@NonNull JLatexMathPlugin.Builder builder) {
// builder builder
// .backgroundProvider(new JLatexMathPlugin.BackgroundProvider() { .backgroundProvider(new JLatexMathPlugin.BackgroundProvider() {
// @NonNull @NonNull
// @Override @Override
// public Drawable provide() { public Drawable provide() {
// return new ColorDrawable(0x40ff0000); return new ColorDrawable(0x40ff0000);
// } }
// }) })
// .fitCanvas(true) .fitCanvas(true)
// .align(JLatexMathDrawable.ALIGN_LEFT) .align(JLatexMathDrawable.ALIGN_CENTER)
// .padding(48) .padding(48)
// ; ;
// } }
// })) }))
.usePlugin(JLatexMathPlugin.create(textView.getTextSize())) // .usePlugin(JLatexMathPlugin.create(textView.getTextSize()))
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void beforeRender(@NonNull Node node) {
Log.e("LTX", DumpNodes.dump(node));
}
})
.build(); .build();
// //
// if (true) { // if (true) {