Merge branch 'f/latex-inline' into develop
This commit is contained in:
commit
f61e0b7b20
@ -4,6 +4,8 @@
|
||||
|
||||
[](https://github.com/noties/Markwon/actions)
|
||||
|
||||

|
||||
|
||||
**Markwon** is a markdown library for Android. It parses markdown
|
||||
following [commonmark-spec] with the help of amazing [commonmark-java]
|
||||
library and renders result as _Android-native_ Spannables. **No HTML**
|
||||
|
@ -20,7 +20,7 @@ public class LinkResolverDef implements LinkResolver {
|
||||
try {
|
||||
context.startActivity(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.w("LinkResolverDef", "Actvity was not found for intent, " + intent.toString());
|
||||
Log.w("LinkResolverDef", "Actvity was not found for the link: '" + link + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ android {
|
||||
dependencies {
|
||||
|
||||
api project(':markwon-core')
|
||||
api project(':markwon-inline-parser')
|
||||
|
||||
api deps['jlatexmath-android']
|
||||
|
||||
|
@ -0,0 +1,50 @@
|
||||
package io.noties.markwon.ext.latex;
|
||||
|
||||
import android.graphics.Rect;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import io.noties.markwon.image.AsyncDrawable;
|
||||
import io.noties.markwon.image.ImageSizeResolver;
|
||||
|
||||
// we must make drawable fit canvas (if specified), but do not keep the ratio whilst scaling up
|
||||
// @since 4.0.0
|
||||
class JLatexBlockImageSizeResolver extends ImageSizeResolver {
|
||||
|
||||
private final boolean fitCanvas;
|
||||
|
||||
JLatexBlockImageSizeResolver(boolean fitCanvas) {
|
||||
this.fitCanvas = fitCanvas;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Rect resolveImageSize(@NonNull AsyncDrawable drawable) {
|
||||
|
||||
final Rect imageBounds = drawable.getResult().getBounds();
|
||||
final int canvasWidth = drawable.getLastKnownCanvasWidth();
|
||||
|
||||
if (fitCanvas) {
|
||||
|
||||
// we modify bounds only if `fitCanvas` is true
|
||||
final int w = imageBounds.width();
|
||||
|
||||
if (w < canvasWidth) {
|
||||
// increase width and center formula (keep height as-is)
|
||||
return new Rect(0, 0, canvasWidth, imageBounds.height());
|
||||
}
|
||||
|
||||
// @since 4.0.2 we additionally scale down the resulting formula (keeping the ratio)
|
||||
// the thing is - JLatexMathDrawable will do it anyway, but it will modify its own
|
||||
// bounds (which AsyncDrawable won't catch), thus leading to an empty space after the formula
|
||||
if (w > canvasWidth) {
|
||||
// here we must scale it down (keeping the ratio)
|
||||
final float ratio = (float) w / imageBounds.height();
|
||||
final int h = (int) (canvasWidth / ratio + .5F);
|
||||
return new Rect(0, 0, canvasWidth, h);
|
||||
}
|
||||
}
|
||||
|
||||
return imageBounds;
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package io.noties.markwon.ext.latex;
|
||||
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
|
||||
import androidx.annotation.IntRange;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import io.noties.markwon.core.MarkwonTheme;
|
||||
import io.noties.markwon.image.AsyncDrawable;
|
||||
import io.noties.markwon.image.AsyncDrawableSpan;
|
||||
|
||||
/**
|
||||
* @since 4.3.0-SNAPSHOT
|
||||
*/
|
||||
class JLatexInlineAsyncDrawableSpan extends AsyncDrawableSpan {
|
||||
|
||||
private final AsyncDrawable drawable;
|
||||
|
||||
JLatexInlineAsyncDrawableSpan(@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;
|
||||
}
|
||||
}
|
@ -1,5 +1,8 @@
|
||||
package io.noties.markwon.ext.latex;
|
||||
|
||||
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;
|
||||
@ -8,13 +11,25 @@ import org.commonmark.parser.block.BlockStart;
|
||||
import org.commonmark.parser.block.MatchedBlockParser;
|
||||
import org.commonmark.parser.block.ParserState;
|
||||
|
||||
/**
|
||||
* @since 4.3.0-SNAPSHOT (although there was a class with the same name,
|
||||
* which is renamed now to {@link JLatexMathBlockParserLegacy})
|
||||
*/
|
||||
public class JLatexMathBlockParser 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 boolean isClosed;
|
||||
private final int signs;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
JLatexMathBlockParser(int signs) {
|
||||
this.signs = signs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Block getBlock() {
|
||||
@ -23,9 +38,19 @@ public class JLatexMathBlockParser extends AbstractBlockParser {
|
||||
|
||||
@Override
|
||||
public BlockContinue tryContinue(ParserState parserState) {
|
||||
final int nextNonSpaceIndex = parserState.getNextNonSpaceIndex();
|
||||
final CharSequence line = parserState.getLine();
|
||||
final int length = line.length();
|
||||
|
||||
if (isClosed) {
|
||||
return BlockContinue.finished();
|
||||
// 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());
|
||||
@ -33,21 +58,8 @@ public class JLatexMathBlockParser extends AbstractBlockParser {
|
||||
|
||||
@Override
|
||||
public void addLine(CharSequence line) {
|
||||
|
||||
if (builder.length() > 0) {
|
||||
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, "");
|
||||
}
|
||||
}
|
||||
builder.append('\n');
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -60,20 +72,49 @@ public class JLatexMathBlockParser extends AbstractBlockParser {
|
||||
@Override
|
||||
public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
|
||||
|
||||
final CharSequence line = state.getLine();
|
||||
final int length = line != null
|
||||
? line.length()
|
||||
: 0;
|
||||
// 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
|
||||
|
||||
if (length > 1) {
|
||||
if ('$' == line.charAt(0)
|
||||
&& '$' == line.charAt(1)) {
|
||||
return BlockStart.of(new JLatexMathBlockParser())
|
||||
.atIndex(state.getIndex() + 2);
|
||||
}
|
||||
final int indent = state.getIndent();
|
||||
|
||||
// check if it's an indented code block
|
||||
if (indent >= Parsing.CODE_BLOCK_INDENT) {
|
||||
return BlockStart.none();
|
||||
}
|
||||
|
||||
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 JLatexMathBlockParser(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,82 @@
|
||||
package io.noties.markwon.ext.latex;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* @since 4.3.0-SNAPSHOT (although it is just renamed parser from previous versions)
|
||||
*/
|
||||
public class JLatexMathBlockParserLegacy extends AbstractBlockParser {
|
||||
|
||||
private final JLatexMathBlock block = new JLatexMathBlock();
|
||||
|
||||
private final StringBuilder builder = new StringBuilder();
|
||||
|
||||
private boolean isClosed;
|
||||
|
||||
@Override
|
||||
public Block getBlock() {
|
||||
return block;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockContinue tryContinue(ParserState parserState) {
|
||||
|
||||
if (isClosed) {
|
||||
return BlockContinue.finished();
|
||||
}
|
||||
|
||||
return BlockContinue.atIndex(parserState.getIndex());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addLine(CharSequence line) {
|
||||
|
||||
if (builder.length() > 0) {
|
||||
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, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closeBlock() {
|
||||
block.latex(builder.toString());
|
||||
}
|
||||
|
||||
public static class Factory extends AbstractBlockParserFactory {
|
||||
|
||||
@Override
|
||||
public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
|
||||
|
||||
final CharSequence line = state.getLine();
|
||||
final int length = line != null
|
||||
? line.length()
|
||||
: 0;
|
||||
|
||||
if (length > 1) {
|
||||
if ('$' == line.charAt(0)
|
||||
&& '$' == line.charAt(1)) {
|
||||
return BlockStart.of(new JLatexMathBlockParserLegacy())
|
||||
.atIndex(state.getIndex() + 2);
|
||||
}
|
||||
}
|
||||
|
||||
return BlockStart.none();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package io.noties.markwon.ext.latex;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.commonmark.node.Node;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import io.noties.markwon.inlineparser.InlineProcessor;
|
||||
|
||||
/**
|
||||
* @since 4.3.0-SNAPSHOT
|
||||
*/
|
||||
public class JLatexMathInlineProcessor extends InlineProcessor {
|
||||
|
||||
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 JLatexMathNode node = new JLatexMathNode();
|
||||
node.latex(latex.substring(2, latex.length() - 2));
|
||||
return node;
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package io.noties.markwon.ext.latex;
|
||||
|
||||
import org.commonmark.node.CustomNode;
|
||||
|
||||
/**
|
||||
* @since 4.3.0-SNAPSHOT
|
||||
*/
|
||||
public class JLatexMathNode extends CustomNode {
|
||||
|
||||
private String latex;
|
||||
|
||||
public String latex() {
|
||||
return latex;
|
||||
}
|
||||
|
||||
public void latex(String latex) {
|
||||
this.latex = latex;
|
||||
}
|
||||
}
|
@ -30,6 +30,7 @@ import io.noties.markwon.image.AsyncDrawableLoader;
|
||||
import io.noties.markwon.image.AsyncDrawableScheduler;
|
||||
import io.noties.markwon.image.AsyncDrawableSpan;
|
||||
import io.noties.markwon.image.ImageSizeResolver;
|
||||
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
|
||||
import ru.noties.jlatexmath.JLatexMathDrawable;
|
||||
|
||||
/**
|
||||
@ -38,11 +39,27 @@ import ru.noties.jlatexmath.JLatexMathDrawable;
|
||||
public class JLatexMathPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
* @since 4.3.0-SNAPSHOT
|
||||
*/
|
||||
public interface BackgroundProvider {
|
||||
@NonNull
|
||||
Drawable provide();
|
||||
public enum RenderMode {
|
||||
/**
|
||||
* <em>LEGACY</em> mode mimics pre {@code 4.3.0-SNAPSHOT} behavior by rendering LaTeX blocks only.
|
||||
* In this mode LaTeX is started by `$$` (that must be exactly at the start of a line) and
|
||||
* ended at whatever line that is ended with `$$` characters exactly.
|
||||
*/
|
||||
LEGACY,
|
||||
|
||||
/**
|
||||
* Starting with {@code 4.3.0-SNAPSHOT} it is possible to have LaTeX inlines (which flows inside
|
||||
* a text paragraph without breaking it). Inline LaTeX starts and ends with `$$` symbols. For example:
|
||||
* {@code
|
||||
* **bold $$\\begin{array}\\end{array}$$ bold-end**, and whatever more
|
||||
* }
|
||||
* LaTeX block starts on a new line started by 0-3 spaces and 2 (or more) {@code $} signs
|
||||
* followed by a new-line (with any amount of space characters in-between). And ends on a new-line
|
||||
* starting with 0-3 spaces followed by number of {@code $} signs that was used to <em>start the block</em>.
|
||||
*/
|
||||
BLOCKS_AND_INLINES
|
||||
}
|
||||
|
||||
public interface BuilderConfigure {
|
||||
@ -54,52 +71,65 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
|
||||
return new JLatexMathPlugin(builder(textSize).build());
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 4.3.0-SNAPSHOT
|
||||
*/
|
||||
@NonNull
|
||||
public static JLatexMathPlugin create(@Px float inlineTextSize, @Px float blockTextSize) {
|
||||
return new JLatexMathPlugin(builder(inlineTextSize, blockTextSize).build());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static JLatexMathPlugin create(@NonNull Config config) {
|
||||
return new JLatexMathPlugin(config);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static JLatexMathPlugin create(float textSize, @NonNull BuilderConfigure builderConfigure) {
|
||||
final Builder builder = new Builder(textSize);
|
||||
public static JLatexMathPlugin create(@Px float textSize, @NonNull BuilderConfigure builderConfigure) {
|
||||
final Builder builder = builder(textSize);
|
||||
builderConfigure.configureBuilder(builder);
|
||||
return new JLatexMathPlugin(builder.build());
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 4.3.0-SNAPSHOT
|
||||
*/
|
||||
@NonNull
|
||||
public static JLatexMathPlugin create(
|
||||
@Px float inlineTextSize,
|
||||
@Px float blockTextSize,
|
||||
@NonNull BuilderConfigure builderConfigure) {
|
||||
final Builder builder = builder(inlineTextSize, blockTextSize);
|
||||
builderConfigure.configureBuilder(builder);
|
||||
return new JLatexMathPlugin(builder.build());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static JLatexMathPlugin.Builder builder(float textSize) {
|
||||
return new Builder(textSize);
|
||||
public static JLatexMathPlugin.Builder builder(@Px float textSize) {
|
||||
return new Builder(JLatexMathTheme.builder(textSize));
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 4.3.0-SNAPSHOT
|
||||
*/
|
||||
@NonNull
|
||||
public static JLatexMathPlugin.Builder builder(@Px float inlineTextSize, @Px float blockTextSize) {
|
||||
return new Builder(JLatexMathTheme.builder(inlineTextSize, blockTextSize));
|
||||
}
|
||||
|
||||
public static class Config {
|
||||
|
||||
private final float textSize;
|
||||
// @since 4.3.0-SNAPSHOT
|
||||
private final JLatexMathTheme theme;
|
||||
|
||||
// @since 4.0.0
|
||||
private final BackgroundProvider backgroundProvider;
|
||||
// @since 4.3.0-SNAPSHOT
|
||||
private final RenderMode renderMode;
|
||||
|
||||
@JLatexMathDrawable.Align
|
||||
private final int align;
|
||||
|
||||
private final boolean fitCanvas;
|
||||
|
||||
// @since 4.0.0
|
||||
private final int paddingHorizontal;
|
||||
|
||||
// @since 4.0.0
|
||||
private final int paddingVertical;
|
||||
|
||||
// @since 4.0.0
|
||||
private final ExecutorService executorService;
|
||||
|
||||
Config(@NonNull Builder builder) {
|
||||
this.textSize = builder.textSize;
|
||||
this.backgroundProvider = builder.backgroundProvider;
|
||||
this.align = builder.align;
|
||||
this.fitCanvas = builder.fitCanvas;
|
||||
this.paddingHorizontal = builder.paddingHorizontal;
|
||||
this.paddingVertical = builder.paddingVertical;
|
||||
|
||||
this.theme = builder.theme.build();
|
||||
this.renderMode = builder.renderMode;
|
||||
// @since 4.0.0
|
||||
ExecutorService executorService = builder.executorService;
|
||||
if (executorService == null) {
|
||||
@ -109,18 +139,51 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
private final Config config;
|
||||
private final JLatextAsyncDrawableLoader jLatextAsyncDrawableLoader;
|
||||
private final JLatexImageSizeResolver jLatexImageSizeResolver;
|
||||
private final JLatexBlockImageSizeResolver jLatexBlockImageSizeResolver;
|
||||
private final ImageSizeResolver inlineImageSizeResolver;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
JLatexMathPlugin(@NonNull Config config) {
|
||||
this.config = config;
|
||||
this.jLatextAsyncDrawableLoader = new JLatextAsyncDrawableLoader(config);
|
||||
this.jLatexImageSizeResolver = new JLatexImageSizeResolver(config.fitCanvas);
|
||||
this.jLatexBlockImageSizeResolver = new JLatexBlockImageSizeResolver(config.theme.blockFitCanvas());
|
||||
this.inlineImageSizeResolver = new InlineImageSizeResolver();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configure(@NonNull Registry registry) {
|
||||
if (RenderMode.BLOCKS_AND_INLINES == config.renderMode) {
|
||||
registry.require(MarkwonInlineParserPlugin.class)
|
||||
.factoryBuilder()
|
||||
.addInlineProcessor(new JLatexMathInlineProcessor());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureParser(@NonNull Parser.Builder builder) {
|
||||
builder.customBlockParserFactory(new JLatexMathBlockParser.Factory());
|
||||
|
||||
// depending on renderMode we should register our parsing here
|
||||
// * for LEGACY -> just add custom block parser
|
||||
// * for INLINE.. -> require InlinePlugin, add inline processor + add block parser
|
||||
|
||||
switch (config.renderMode) {
|
||||
|
||||
case LEGACY: {
|
||||
builder.customBlockParserFactory(new JLatexMathBlockParserLegacy.Factory());
|
||||
}
|
||||
break;
|
||||
|
||||
case BLOCKS_AND_INLINES: {
|
||||
builder.customBlockParserFactory(new JLatexMathBlockParser.Factory());
|
||||
// inline processor is added through `registry`
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new RuntimeException("Unexpected `renderMode`: " + config.renderMode);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -129,6 +192,8 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull JLatexMathBlock jLatexMathBlock) {
|
||||
|
||||
visitor.ensureNewLine();
|
||||
|
||||
final String latex = jLatexMathBlock.latex();
|
||||
|
||||
final int length = visitor.length();
|
||||
@ -142,17 +207,56 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
final AsyncDrawableSpan span = new AsyncDrawableSpan(
|
||||
configuration.theme(),
|
||||
new AsyncDrawable(
|
||||
new JLatextAsyncDrawable(
|
||||
latex,
|
||||
jLatextAsyncDrawableLoader,
|
||||
jLatexImageSizeResolver,
|
||||
null),
|
||||
AsyncDrawableSpan.ALIGN_BOTTOM,
|
||||
jLatexBlockImageSizeResolver,
|
||||
null,
|
||||
true),
|
||||
AsyncDrawableSpan.ALIGN_CENTER,
|
||||
false);
|
||||
|
||||
visitor.setSpans(length, span);
|
||||
|
||||
if (visitor.hasNext(jLatexMathBlock)) {
|
||||
visitor.ensureNewLine();
|
||||
visitor.forceNewLine();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (RenderMode.BLOCKS_AND_INLINES == config.renderMode) {
|
||||
|
||||
builder.on(JLatexMathNode.class, new MarkwonVisitor.NodeVisitor<JLatexMathNode>() {
|
||||
@Override
|
||||
public void visit(@NonNull MarkwonVisitor visitor, @NonNull JLatexMathNode jLatexMathNode) {
|
||||
final String latex = jLatexMathNode.latex();
|
||||
|
||||
final int length = visitor.length();
|
||||
|
||||
// @since 4.0.2 we cannot append _raw_ latex as a placeholder-text,
|
||||
// because Android will draw formula for each line of text, thus
|
||||
// leading to formula duplicated (drawn on each line of text)
|
||||
visitor.builder().append(prepareLatexTextPlaceholder(latex));
|
||||
|
||||
final MarkwonConfiguration configuration = visitor.configuration();
|
||||
|
||||
final AsyncDrawableSpan span = new JLatexInlineAsyncDrawableSpan(
|
||||
configuration.theme(),
|
||||
new JLatextAsyncDrawable(
|
||||
latex,
|
||||
jLatextAsyncDrawableLoader,
|
||||
inlineImageSizeResolver,
|
||||
null,
|
||||
false),
|
||||
AsyncDrawableSpan.ALIGN_CENTER,
|
||||
false);
|
||||
|
||||
visitor.setSpans(length, span);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -174,61 +278,30 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private final float textSize;
|
||||
// @since 4.3.0-SNAPSHOT
|
||||
private final JLatexMathTheme.Builder theme;
|
||||
|
||||
// @since 4.0.0
|
||||
private BackgroundProvider backgroundProvider;
|
||||
|
||||
@JLatexMathDrawable.Align
|
||||
private int align = JLatexMathDrawable.ALIGN_CENTER;
|
||||
|
||||
private boolean fitCanvas = true;
|
||||
|
||||
// @since 4.0.0
|
||||
private int paddingHorizontal;
|
||||
|
||||
// @since 4.0.0
|
||||
private int paddingVertical;
|
||||
// @since 4.3.0-SNAPSHOT
|
||||
private RenderMode renderMode = RenderMode.BLOCKS_AND_INLINES;
|
||||
|
||||
// @since 4.0.0
|
||||
private ExecutorService executorService;
|
||||
|
||||
Builder(float textSize) {
|
||||
this.textSize = textSize;
|
||||
Builder(@NonNull JLatexMathTheme.Builder builder) {
|
||||
this.theme = builder;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder backgroundProvider(@NonNull BackgroundProvider backgroundProvider) {
|
||||
this.backgroundProvider = backgroundProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder align(@JLatexMathDrawable.Align int align) {
|
||||
this.align = align;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder fitCanvas(boolean fitCanvas) {
|
||||
this.fitCanvas = fitCanvas;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder padding(@Px int padding) {
|
||||
this.paddingHorizontal = padding;
|
||||
this.paddingVertical = padding;
|
||||
return this;
|
||||
public JLatexMathTheme.Builder theme() {
|
||||
return theme;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 4.0.0
|
||||
* @since 4.3.0-SNAPSHOT
|
||||
*/
|
||||
@NonNull
|
||||
public Builder builder(@Px int paddingHorizontal, @Px int paddingVertical) {
|
||||
this.paddingHorizontal = paddingHorizontal;
|
||||
this.paddingVertical = paddingVertical;
|
||||
public Builder renderMode(@NonNull RenderMode renderMode) {
|
||||
this.renderMode = renderMode;
|
||||
return this;
|
||||
}
|
||||
|
||||
@ -248,7 +321,7 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
|
||||
}
|
||||
|
||||
// @since 4.0.0
|
||||
private static class JLatextAsyncDrawableLoader extends AsyncDrawableLoader {
|
||||
static class JLatextAsyncDrawableLoader extends AsyncDrawableLoader {
|
||||
|
||||
private final Config config;
|
||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||
@ -287,23 +360,15 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
private void execute() {
|
||||
|
||||
// @since 4.0.1 (background provider can be null)
|
||||
final BackgroundProvider backgroundProvider = config.backgroundProvider;
|
||||
final JLatexMathDrawable jLatexMathDrawable;
|
||||
|
||||
// create JLatexMathDrawable
|
||||
//noinspection ConstantConditions
|
||||
final JLatexMathDrawable 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();
|
||||
final JLatextAsyncDrawable jLatextAsyncDrawable = (JLatextAsyncDrawable) drawable;
|
||||
|
||||
if (jLatextAsyncDrawable.isBlock()) {
|
||||
jLatexMathDrawable = createBlockDrawable(jLatextAsyncDrawable.getDestination());
|
||||
} else {
|
||||
jLatexMathDrawable = createInlineDrawable(jLatextAsyncDrawable.getDestination());
|
||||
}
|
||||
|
||||
// we must post to handler, but also have a way to identify the drawable
|
||||
// for which we are posting (in case of cancellation)
|
||||
@ -342,47 +407,63 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin {
|
||||
public Drawable placeholder(@NonNull AsyncDrawable drawable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// @since 4.3.0-SNAPSHOT
|
||||
@NonNull
|
||||
private JLatexMathDrawable createBlockDrawable(@NonNull String latex) {
|
||||
|
||||
final JLatexMathTheme theme = config.theme;
|
||||
|
||||
final JLatexMathTheme.BackgroundProvider backgroundProvider = theme.blockBackgroundProvider();
|
||||
final JLatexMathTheme.Padding padding = theme.blockPadding();
|
||||
|
||||
final JLatexMathDrawable.Builder builder = JLatexMathDrawable.builder(latex)
|
||||
.textSize(theme.blockTextSize())
|
||||
.align(theme.blockHorizontalAlignment())
|
||||
.fitCanvas(theme.blockFitCanvas());
|
||||
|
||||
if (backgroundProvider != null) {
|
||||
builder.background(backgroundProvider.provide());
|
||||
}
|
||||
|
||||
if (padding != null) {
|
||||
builder.padding(padding.left, padding.top, padding.right, padding.bottom);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
// @since 4.3.0-SNAPSHOT
|
||||
@NonNull
|
||||
private JLatexMathDrawable createInlineDrawable(@NonNull String latex) {
|
||||
|
||||
final JLatexMathTheme theme = config.theme;
|
||||
|
||||
final JLatexMathTheme.BackgroundProvider backgroundProvider = theme.inlineBackgroundProvider();
|
||||
final JLatexMathTheme.Padding padding = theme.inlinePadding();
|
||||
|
||||
final JLatexMathDrawable.Builder builder = JLatexMathDrawable.builder(latex)
|
||||
.textSize(theme.inlineTextSize())
|
||||
.fitCanvas(false);
|
||||
|
||||
if (backgroundProvider != null) {
|
||||
builder.background(backgroundProvider.provide());
|
||||
}
|
||||
|
||||
if (padding != null) {
|
||||
builder.padding(padding.left, padding.top, padding.right, padding.bottom);
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
|
||||
// we must make drawable fit canvas (if specified), but do not keep the ratio whilst scaling up
|
||||
// @since 4.0.0
|
||||
private static class JLatexImageSizeResolver extends ImageSizeResolver {
|
||||
|
||||
private final boolean fitCanvas;
|
||||
|
||||
JLatexImageSizeResolver(boolean fitCanvas) {
|
||||
this.fitCanvas = fitCanvas;
|
||||
}
|
||||
private static class InlineImageSizeResolver extends ImageSizeResolver {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Rect resolveImageSize(@NonNull AsyncDrawable drawable) {
|
||||
|
||||
final Rect imageBounds = drawable.getResult().getBounds();
|
||||
final int canvasWidth = drawable.getLastKnownCanvasWidth();
|
||||
|
||||
if (fitCanvas) {
|
||||
|
||||
// we modify bounds only if `fitCanvas` is true
|
||||
final int w = imageBounds.width();
|
||||
|
||||
if (w < canvasWidth) {
|
||||
// increase width and center formula (keep height as-is)
|
||||
return new Rect(0, 0, canvasWidth, imageBounds.height());
|
||||
}
|
||||
|
||||
// @since 4.0.2 we additionally scale down the resulting formula (keeping the ratio)
|
||||
// the thing is - JLatexMathDrawable will do it anyway, but it will modify its own
|
||||
// bounds (which AsyncDrawable won't catch), thus leading to an empty space after the formula
|
||||
if (w > canvasWidth) {
|
||||
// here we must scale it down (keeping the ratio)
|
||||
final float ratio = (float) w / imageBounds.height();
|
||||
final int h = (int) (canvasWidth / ratio + .5F);
|
||||
return new Rect(0, 0, canvasWidth, h);
|
||||
}
|
||||
}
|
||||
|
||||
return imageBounds;
|
||||
return drawable.getResult().getBounds();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,297 @@
|
||||
package io.noties.markwon.ext.latex;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.Px;
|
||||
|
||||
import ru.noties.jlatexmath.JLatexMathDrawable;
|
||||
|
||||
/**
|
||||
* @since 4.3.0-SNAPSHOT
|
||||
*/
|
||||
public abstract class JLatexMathTheme {
|
||||
|
||||
@NonNull
|
||||
public static JLatexMathTheme create(@Px float textSize) {
|
||||
return builder(textSize).build();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static JLatexMathTheme create(@Px float inlineTextSize, @Px float blockTextSize) {
|
||||
return builder(inlineTextSize, blockTextSize).build();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static JLatexMathTheme.Builder builder(@Px float textSize) {
|
||||
return new JLatexMathTheme.Builder(textSize, 0F, 0F);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static JLatexMathTheme.Builder builder(@Px float inlineTextSize, @Px float blockTextSize) {
|
||||
return new Builder(0F, inlineTextSize, blockTextSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moved from {@link JLatexMathPlugin} in {@code 4.3.0-SNAPSHOT} version
|
||||
*
|
||||
* @since 4.0.0
|
||||
*/
|
||||
public interface BackgroundProvider {
|
||||
@NonNull
|
||||
Drawable provide();
|
||||
}
|
||||
|
||||
/**
|
||||
* Special immutable class to hold padding information
|
||||
*/
|
||||
public static class Padding {
|
||||
public final int left;
|
||||
public final int top;
|
||||
public final int right;
|
||||
public final int bottom;
|
||||
|
||||
public Padding(int left, int top, int right, int bottom) {
|
||||
this.left = left;
|
||||
this.top = top;
|
||||
this.right = right;
|
||||
this.bottom = bottom;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Padding{" +
|
||||
"left=" + left +
|
||||
", top=" + top +
|
||||
", right=" + right +
|
||||
", bottom=" + bottom +
|
||||
'}';
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Padding all(int value) {
|
||||
return new Padding(value, value, value, value);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Padding symmetric(int vertical, int horizontal) {
|
||||
return new Padding(horizontal, vertical, horizontal, vertical);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return text size in pixels for <strong>inline LaTeX</strong>
|
||||
* @see #blockTextSize()
|
||||
*/
|
||||
@Px
|
||||
public abstract float inlineTextSize();
|
||||
|
||||
/**
|
||||
* @return text size in pixels for <strong>block LaTeX</strong>
|
||||
* @see #inlineTextSize()
|
||||
*/
|
||||
@Px
|
||||
public abstract float blockTextSize();
|
||||
|
||||
@Nullable
|
||||
public abstract BackgroundProvider inlineBackgroundProvider();
|
||||
|
||||
@Nullable
|
||||
public abstract BackgroundProvider blockBackgroundProvider();
|
||||
|
||||
/**
|
||||
* @return boolean if <strong>block LaTeX</strong> must fit the width of canvas
|
||||
*/
|
||||
public abstract boolean blockFitCanvas();
|
||||
|
||||
/**
|
||||
* @return horizontal alignment of <strong>block LaTeX</strong> if {@link #blockFitCanvas()}
|
||||
* is enabled (thus space for alignment is available)
|
||||
*/
|
||||
@JLatexMathDrawable.Align
|
||||
public abstract int blockHorizontalAlignment();
|
||||
|
||||
@Nullable
|
||||
public abstract Padding inlinePadding();
|
||||
|
||||
@Nullable
|
||||
public abstract Padding blockPadding();
|
||||
|
||||
|
||||
public static class Builder {
|
||||
private final float textSize;
|
||||
private final float inlineTextSize;
|
||||
private final float blockTextSize;
|
||||
|
||||
private BackgroundProvider backgroundProvider;
|
||||
private BackgroundProvider inlineBackgroundProvider;
|
||||
private BackgroundProvider blockBackgroundProvider;
|
||||
|
||||
private boolean blockFitCanvas = true;
|
||||
// horizontal alignment (when there is additional horizontal space)
|
||||
private int blockHorizontalAlignment = JLatexMathDrawable.ALIGN_CENTER;
|
||||
|
||||
private Padding padding;
|
||||
private Padding inlinePadding;
|
||||
private Padding blockPadding;
|
||||
|
||||
Builder(float textSize, float inlineTextSize, float blockTextSize) {
|
||||
this.textSize = textSize;
|
||||
this.inlineTextSize = inlineTextSize;
|
||||
this.blockTextSize = blockTextSize;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder backgroundProvider(@Nullable BackgroundProvider backgroundProvider) {
|
||||
this.backgroundProvider = backgroundProvider;
|
||||
this.inlineBackgroundProvider = backgroundProvider;
|
||||
this.blockBackgroundProvider = backgroundProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder inlineBackgroundProvider(@Nullable BackgroundProvider inlineBackgroundProvider) {
|
||||
this.inlineBackgroundProvider = inlineBackgroundProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder blockBackgroundProvider(@Nullable BackgroundProvider blockBackgroundProvider) {
|
||||
this.blockBackgroundProvider = blockBackgroundProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder blockFitCanvas(boolean blockFitCanvas) {
|
||||
this.blockFitCanvas = blockFitCanvas;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder blockHorizontalAlignment(@JLatexMathDrawable.Align int blockHorizontalAlignment) {
|
||||
this.blockHorizontalAlignment = blockHorizontalAlignment;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder padding(@Nullable Padding padding) {
|
||||
this.padding = padding;
|
||||
this.inlinePadding = padding;
|
||||
this.blockPadding = padding;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder inlinePadding(@Nullable Padding inlinePadding) {
|
||||
this.inlinePadding = inlinePadding;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder blockPadding(@Nullable Padding blockPadding) {
|
||||
this.blockPadding = blockPadding;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public JLatexMathTheme build() {
|
||||
return new Impl(this);
|
||||
}
|
||||
}
|
||||
|
||||
static class Impl extends JLatexMathTheme {
|
||||
|
||||
private final float textSize;
|
||||
private final float inlineTextSize;
|
||||
private final float blockTextSize;
|
||||
|
||||
private final BackgroundProvider backgroundProvider;
|
||||
private final BackgroundProvider inlineBackgroundProvider;
|
||||
private final BackgroundProvider blockBackgroundProvider;
|
||||
|
||||
private final boolean blockFitCanvas;
|
||||
// horizontal alignment (when there is additional horizontal space)
|
||||
private int blockHorizontalAlignment;
|
||||
|
||||
private final Padding padding;
|
||||
private final Padding inlinePadding;
|
||||
private final Padding blockPadding;
|
||||
|
||||
Impl(@NonNull Builder builder) {
|
||||
this.textSize = builder.textSize;
|
||||
this.inlineTextSize = builder.inlineTextSize;
|
||||
this.blockTextSize = builder.blockTextSize;
|
||||
this.backgroundProvider = builder.backgroundProvider;
|
||||
this.inlineBackgroundProvider = builder.inlineBackgroundProvider;
|
||||
this.blockBackgroundProvider = builder.blockBackgroundProvider;
|
||||
this.blockFitCanvas = builder.blockFitCanvas;
|
||||
this.blockHorizontalAlignment = builder.blockHorizontalAlignment;
|
||||
this.padding = builder.padding;
|
||||
this.inlinePadding = builder.inlinePadding;
|
||||
this.blockPadding = builder.blockPadding;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float inlineTextSize() {
|
||||
if (inlineTextSize > 0F) {
|
||||
return inlineTextSize;
|
||||
}
|
||||
return textSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float blockTextSize() {
|
||||
if (blockTextSize > 0F) {
|
||||
return blockTextSize;
|
||||
}
|
||||
return textSize;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public BackgroundProvider inlineBackgroundProvider() {
|
||||
if (inlineBackgroundProvider != null) {
|
||||
return inlineBackgroundProvider;
|
||||
}
|
||||
return backgroundProvider;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public BackgroundProvider blockBackgroundProvider() {
|
||||
if (blockBackgroundProvider != null) {
|
||||
return blockBackgroundProvider;
|
||||
}
|
||||
return backgroundProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean blockFitCanvas() {
|
||||
return blockFitCanvas;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int blockHorizontalAlignment() {
|
||||
return blockHorizontalAlignment;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Padding inlinePadding() {
|
||||
if (inlinePadding != null) {
|
||||
return inlinePadding;
|
||||
}
|
||||
return padding;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Padding blockPadding() {
|
||||
if (blockPadding != null) {
|
||||
return blockPadding;
|
||||
}
|
||||
return padding;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package io.noties.markwon.ext.latex;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import io.noties.markwon.image.AsyncDrawable;
|
||||
import io.noties.markwon.image.AsyncDrawableLoader;
|
||||
import io.noties.markwon.image.ImageSize;
|
||||
import io.noties.markwon.image.ImageSizeResolver;
|
||||
|
||||
/**
|
||||
* @since 4.3.0-SNAPSHOT
|
||||
*/
|
||||
class JLatextAsyncDrawable extends AsyncDrawable {
|
||||
|
||||
private final boolean isBlock;
|
||||
|
||||
JLatextAsyncDrawable(
|
||||
@NonNull String destination,
|
||||
@NonNull AsyncDrawableLoader loader,
|
||||
@NonNull ImageSizeResolver imageSizeResolver,
|
||||
@Nullable ImageSize imageSize,
|
||||
boolean isBlock
|
||||
) {
|
||||
super(destination, loader, imageSizeResolver, imageSize);
|
||||
this.isBlock = isBlock;
|
||||
}
|
||||
|
||||
public boolean isBlock() {
|
||||
return isBlock;
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api project(':markwon-core')
|
||||
api deps['x-annotations']
|
||||
api deps['commonmark']
|
||||
|
||||
|
@ -0,0 +1,59 @@
|
||||
package io.noties.markwon.inlineparser;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.commonmark.parser.Parser;
|
||||
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
|
||||
/**
|
||||
* @since 4.3.0-SNAPSHOT
|
||||
*/
|
||||
public class MarkwonInlineParserPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
public interface BuilderConfigure<B extends MarkwonInlineParser.FactoryBuilder> {
|
||||
void configureBuilder(@NonNull B factoryBuilder);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static MarkwonInlineParserPlugin create() {
|
||||
return create(MarkwonInlineParser.factoryBuilder());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static MarkwonInlineParserPlugin create(@NonNull BuilderConfigure<MarkwonInlineParser.FactoryBuilder> configure) {
|
||||
final MarkwonInlineParser.FactoryBuilder factoryBuilder = MarkwonInlineParser.factoryBuilder();
|
||||
configure.configureBuilder(factoryBuilder);
|
||||
return new MarkwonInlineParserPlugin(factoryBuilder);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static MarkwonInlineParserPlugin create(@NonNull MarkwonInlineParser.FactoryBuilder factoryBuilder) {
|
||||
return new MarkwonInlineParserPlugin(factoryBuilder);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static <B extends MarkwonInlineParser.FactoryBuilder> MarkwonInlineParserPlugin create(
|
||||
@NonNull B factoryBuilder,
|
||||
@NonNull BuilderConfigure<B> configure) {
|
||||
configure.configureBuilder(factoryBuilder);
|
||||
return new MarkwonInlineParserPlugin(factoryBuilder);
|
||||
}
|
||||
|
||||
private final MarkwonInlineParser.FactoryBuilder factoryBuilder;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
MarkwonInlineParserPlugin(@NonNull MarkwonInlineParser.FactoryBuilder factoryBuilder) {
|
||||
this.factoryBuilder = factoryBuilder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureParser(@NonNull Parser.Builder builder) {
|
||||
builder.inlineParserFactory(factoryBuilder.build());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public MarkwonInlineParser.FactoryBuilder factoryBuilder() {
|
||||
return factoryBuilder;
|
||||
}
|
||||
}
|
@ -49,6 +49,7 @@ dependencies {
|
||||
implementation project(':markwon-syntax-highlight')
|
||||
|
||||
implementation project(':markwon-image-picasso')
|
||||
implementation project(':markwon-image-glide')
|
||||
|
||||
deps.with {
|
||||
implementation it['x-recycler-view']
|
||||
|
@ -35,6 +35,7 @@
|
||||
|
||||
<activity android:name=".inlineparser.InlineParserActivity" />
|
||||
<activity android:name=".htmldetails.HtmlDetailsActivity" />
|
||||
<activity android:name=".tasklist.TaskListActivity" />
|
||||
|
||||
</application>
|
||||
|
||||
|
@ -0,0 +1,49 @@
|
||||
package io.noties.markwon.sample;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public abstract class ActivityWithMenuOptions extends Activity {
|
||||
|
||||
@NonNull
|
||||
public abstract MenuOptions menuOptions();
|
||||
|
||||
protected void beforeOptionSelected(@NonNull String option) {
|
||||
// no op, override to customize
|
||||
}
|
||||
|
||||
protected void afterOptionSelected(@NonNull String option) {
|
||||
// no op, override to customize
|
||||
}
|
||||
|
||||
private MenuOptions menuOptions;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
menuOptions = menuOptions();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
return menuOptions.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
final MenuOptions.Option option = menuOptions.onOptionsItemSelected(item);
|
||||
if (option != null) {
|
||||
beforeOptionSelected(option.title);
|
||||
option.action.run();
|
||||
afterOptionSelected(option.title);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
@ -30,6 +30,7 @@ import io.noties.markwon.sample.latex.LatexActivity;
|
||||
import io.noties.markwon.sample.precomputed.PrecomputedActivity;
|
||||
import io.noties.markwon.sample.recycler.RecyclerActivity;
|
||||
import io.noties.markwon.sample.simpleext.SimpleExtActivity;
|
||||
import io.noties.markwon.sample.tasklist.TaskListActivity;
|
||||
|
||||
public class MainActivity extends Activity {
|
||||
|
||||
@ -132,6 +133,10 @@ public class MainActivity extends Activity {
|
||||
activity = HtmlDetailsActivity.class;
|
||||
break;
|
||||
|
||||
case TASK_LIST:
|
||||
activity = TaskListActivity.class;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException("No Activity is associated with sample-item: " + item);
|
||||
}
|
||||
|
@ -0,0 +1,57 @@
|
||||
package io.noties.markwon.sample;
|
||||
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class MenuOptions {
|
||||
|
||||
@NonNull
|
||||
public static MenuOptions create() {
|
||||
return new MenuOptions();
|
||||
}
|
||||
|
||||
static class Option {
|
||||
final String title;
|
||||
final Runnable action;
|
||||
|
||||
Option(@NonNull String title, @NonNull Runnable action) {
|
||||
this.title = title;
|
||||
this.action = action;
|
||||
}
|
||||
}
|
||||
|
||||
// to preserve order use LinkedHashMap
|
||||
private final Map<String, Runnable> actions = new LinkedHashMap<>();
|
||||
|
||||
@NonNull
|
||||
public MenuOptions add(@NonNull String title, @NonNull Runnable action) {
|
||||
actions.put(title, action);
|
||||
return this;
|
||||
}
|
||||
|
||||
boolean onCreateOptionsMenu(Menu menu) {
|
||||
if (!actions.isEmpty()) {
|
||||
for (String key : actions.keySet()) {
|
||||
menu.add(key);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Option onOptionsItemSelected(MenuItem item) {
|
||||
final String title = String.valueOf(item.getTitle());
|
||||
final Runnable action = actions.get(title);
|
||||
if (action != null) {
|
||||
return new Option(title, action);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -27,7 +27,9 @@ public enum Sample {
|
||||
|
||||
INLINE_PARSER(R.string.sample_inline_parser),
|
||||
|
||||
HTML_DETAILS(R.string.sample_html_details);
|
||||
HTML_DETAILS(R.string.sample_html_details),
|
||||
|
||||
TASK_LIST(R.string.sample_task_list);
|
||||
|
||||
private final int textResId;
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
package io.noties.markwon.sample.editor;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextPaint;
|
||||
import android.text.TextUtils;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.MetricAffectingSpan;
|
||||
@ -41,30 +41,61 @@ import io.noties.markwon.inlineparser.BangInlineProcessor;
|
||||
import io.noties.markwon.inlineparser.EntityInlineProcessor;
|
||||
import io.noties.markwon.inlineparser.HtmlInlineProcessor;
|
||||
import io.noties.markwon.inlineparser.MarkwonInlineParser;
|
||||
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
|
||||
import io.noties.markwon.linkify.LinkifyPlugin;
|
||||
import io.noties.markwon.sample.ActivityWithMenuOptions;
|
||||
import io.noties.markwon.sample.MenuOptions;
|
||||
import io.noties.markwon.sample.R;
|
||||
|
||||
public class EditorActivity extends Activity {
|
||||
public class EditorActivity extends ActivityWithMenuOptions {
|
||||
|
||||
private EditText editText;
|
||||
private String pendingInput;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MenuOptions menuOptions() {
|
||||
return MenuOptions.create()
|
||||
.add("simpleProcess", this::simple_process)
|
||||
.add("simplePreRender", this::simple_pre_render)
|
||||
.add("customPunctuationSpan", this::custom_punctuation_span)
|
||||
.add("additionalEditSpan", this::additional_edit_span)
|
||||
.add("additionalPlugins", this::additional_plugins)
|
||||
.add("multipleEditSpans", this::multiple_edit_spans)
|
||||
.add("multipleEditSpansPlugin", this::multiple_edit_spans_plugin)
|
||||
.add("pluginRequire", this::plugin_require)
|
||||
.add("pluginNoDefaults", this::plugin_no_defaults);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void beforeOptionSelected(@NonNull String option) {
|
||||
// we cannot _clear_ editText of text-watchers without keeping a reference to them...
|
||||
pendingInput = editText != null
|
||||
? editText.getText().toString()
|
||||
: null;
|
||||
|
||||
createView();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void afterOptionSelected(@NonNull String option) {
|
||||
if (!TextUtils.isEmpty(pendingInput)) {
|
||||
editText.setText(pendingInput);
|
||||
}
|
||||
}
|
||||
|
||||
private void createView() {
|
||||
setContentView(R.layout.activity_editor);
|
||||
|
||||
this.editText = findViewById(R.id.edit_text);
|
||||
|
||||
initBottomBar();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_editor);
|
||||
|
||||
this.editText = findViewById(R.id.edit_text);
|
||||
initBottomBar();
|
||||
|
||||
// simple_process();
|
||||
|
||||
// simple_pre_render();
|
||||
|
||||
// custom_punctuation_span();
|
||||
|
||||
// additional_edit_span();
|
||||
|
||||
// additional_plugins();
|
||||
createView();
|
||||
|
||||
multiple_edit_spans();
|
||||
}
|
||||
@ -216,6 +247,76 @@ public class EditorActivity extends Activity {
|
||||
editor, Executors.newSingleThreadExecutor(), editText));
|
||||
}
|
||||
|
||||
private void multiple_edit_spans_plugin() {
|
||||
// inline parsing is configured via MarkwonInlineParserPlugin
|
||||
|
||||
// for links to be clickable
|
||||
editText.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
|
||||
final Markwon markwon = Markwon.builder(this)
|
||||
.usePlugin(StrikethroughPlugin.create())
|
||||
.usePlugin(LinkifyPlugin.create())
|
||||
.usePlugin(MarkwonInlineParserPlugin.create(builder -> {
|
||||
builder
|
||||
.excludeInlineProcessor(BangInlineProcessor.class)
|
||||
.excludeInlineProcessor(HtmlInlineProcessor.class)
|
||||
.excludeInlineProcessor(EntityInlineProcessor.class);
|
||||
}))
|
||||
.build();
|
||||
|
||||
final LinkEditHandler.OnClick onClick = (widget, link) -> markwon.configuration().linkResolver().resolve(widget, link);
|
||||
|
||||
final MarkwonEditor editor = MarkwonEditor.builder(markwon)
|
||||
.useEditHandler(new EmphasisEditHandler())
|
||||
.useEditHandler(new StrongEmphasisEditHandler())
|
||||
.useEditHandler(new StrikethroughEditHandler())
|
||||
.useEditHandler(new CodeEditHandler())
|
||||
.useEditHandler(new BlockQuoteEditHandler())
|
||||
.useEditHandler(new LinkEditHandler(onClick))
|
||||
.build();
|
||||
|
||||
editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
|
||||
editor, Executors.newSingleThreadExecutor(), editText));
|
||||
}
|
||||
|
||||
private void plugin_require() {
|
||||
// usage of plugin from other plugins
|
||||
|
||||
final Markwon markwon = Markwon.builder(this)
|
||||
.usePlugin(MarkwonInlineParserPlugin.create())
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configure(@NonNull Registry registry) {
|
||||
registry.require(MarkwonInlineParserPlugin.class)
|
||||
.factoryBuilder()
|
||||
.excludeInlineProcessor(HtmlInlineProcessor.class);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
final MarkwonEditor editor = MarkwonEditor.create(markwon);
|
||||
|
||||
editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
|
||||
editor, Executors.newSingleThreadExecutor(), editText));
|
||||
}
|
||||
|
||||
private void plugin_no_defaults() {
|
||||
// a plugin with no defaults registered
|
||||
|
||||
final Markwon markwon = Markwon.builder(this)
|
||||
.usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults()))
|
||||
// .usePlugin(MarkwonInlineParserPlugin.create(MarkwonInlineParser.factoryBuilderNoDefaults(), factoryBuilder -> {
|
||||
// // if anything, they can be included here
|
||||
//// factoryBuilder.includeDefaults()
|
||||
// }))
|
||||
.build();
|
||||
|
||||
final MarkwonEditor editor = MarkwonEditor.create(markwon);
|
||||
|
||||
editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
|
||||
editor, Executors.newSingleThreadExecutor(), editText));
|
||||
}
|
||||
|
||||
private void initBottomBar() {
|
||||
// all except block-quote wraps if have selection, or inserts at current cursor position
|
||||
|
||||
|
@ -40,24 +40,28 @@ class LinkEditHandler extends AbstractEditHandler<LinkSpan> {
|
||||
final EditLinkSpan editLinkSpan = persistedSpans.get(EditLinkSpan.class);
|
||||
editLinkSpan.link = span.getLink();
|
||||
|
||||
final int s;
|
||||
final int e;
|
||||
// First first __letter__ to find link content (scheme start in URL, receiver in email address)
|
||||
// NB! do not use phone number auto-link (via LinkifyPlugin) as we cannot guarantee proper link
|
||||
// display. For example, we _could_ also look for a digit, but:
|
||||
// * if phone number start with special symbol, we won't have it (`+`, `(`)
|
||||
// * it might interfere with an ordered-list
|
||||
int start = -1;
|
||||
|
||||
// markdown link vs. autolink
|
||||
if ('[' == input.charAt(spanStart)) {
|
||||
s = spanStart + 1;
|
||||
e = spanStart + 1 + spanTextLength;
|
||||
} else {
|
||||
s = spanStart;
|
||||
e = spanStart + spanTextLength;
|
||||
for (int i = spanStart, length = input.length(); i < length; i++) {
|
||||
if (Character.isLetter(input.charAt(i))) {
|
||||
start = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
editable.setSpan(
|
||||
editLinkSpan,
|
||||
s,
|
||||
e,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
if (start > -1) {
|
||||
editable.setSpan(
|
||||
editLinkSpan,
|
||||
start,
|
||||
start + spanTextLength,
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
@ -1,8 +1,7 @@
|
||||
package io.noties.markwon.sample.latex;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.widget.TextView;
|
||||
|
||||
@ -11,19 +10,17 @@ import androidx.annotation.Nullable;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.ext.latex.JLatexMathPlugin;
|
||||
import io.noties.markwon.ext.latex.JLatexMathTheme;
|
||||
import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin;
|
||||
import io.noties.markwon.sample.ActivityWithMenuOptions;
|
||||
import io.noties.markwon.sample.MenuOptions;
|
||||
import io.noties.markwon.sample.R;
|
||||
import ru.noties.jlatexmath.JLatexMathDrawable;
|
||||
|
||||
public class LatexActivity extends Activity {
|
||||
public class LatexActivity extends ActivityWithMenuOptions {
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_text_view);
|
||||
|
||||
final TextView textView = findViewById(R.id.text_view);
|
||||
private static final String LATEX_ARRAY;
|
||||
|
||||
static {
|
||||
String latex = "\\begin{array}{l}";
|
||||
latex += "\\forall\\varepsilon\\in\\mathbb{R}_+^*\\ \\exists\\eta>0\\ |x-x_0|\\leq\\eta\\Longrightarrow|f(x)-f(x_0)|\\leq\\varepsilon\\\\";
|
||||
latex += "\\det\\begin{bmatrix}a_{11}&a_{12}&\\cdots&a_{1n}\\\\a_{21}&\\ddots&&\\vdots\\\\\\vdots&&\\ddots&\\vdots\\\\a_{n1}&\\cdots&\\cdots&a_{nn}\\end{bmatrix}\\overset{\\mathrm{def}}{=}\\sum_{\\sigma\\in\\mathfrak{S}_n}\\varepsilon(\\sigma)\\prod_{k=1}^n a_{k\\sigma(k)}\\\\";
|
||||
@ -34,61 +31,118 @@ public class LatexActivity extends Activity {
|
||||
latex += "L = \\int_a^b \\sqrt{ \\left|\\sum_{i,j=1}^ng_{ij}(\\gamma(t))\\left(\\frac{d}{dt}x^i\\circ\\gamma(t)\\right)\\left(\\frac{d}{dt}x^j\\circ\\gamma(t)\\right)\\right|}\\,dt\\\\";
|
||||
latex += "\\begin{array}{rl} s &= \\int_a^b\\left\\|\\frac{d}{dt}\\vec{r}\\,(u(t),v(t))\\right\\|\\,dt \\\\ &= \\int_a^b \\sqrt{u'(t)^2\\,\\vec{r}_u\\cdot\\vec{r}_u + 2u'(t)v'(t)\\, \\vec{r}_u\\cdot\\vec{r}_v+ v'(t)^2\\,\\vec{r}_v\\cdot\\vec{r}_v}\\,\\,\\, dt. \\end{array}\\\\";
|
||||
latex += "\\end{array}";
|
||||
LATEX_ARRAY = latex;
|
||||
}
|
||||
|
||||
// String latex = "\\text{A long division \\longdiv{12345}{13}";
|
||||
// String latex = "{a \\bangle b} {c \\brace d} {e \\brack f} {g \\choose h}";
|
||||
private static final String LATEX_LONG_DIVISION = "\\text{A long division \\longdiv{12345}{13}";
|
||||
private static final String LATEX_BANGLE = "{a \\bangle b} {c \\brace d} {e \\brack f} {g \\choose h}";
|
||||
private static final String LATEX_BOXES;
|
||||
|
||||
// String latex = "\\begin{array}{cc}";
|
||||
// latex += "\\fbox{\\text{A framed box with \\textdbend}}&\\shadowbox{\\text{A shadowed box}}\\cr";
|
||||
// latex += "\\doublebox{\\text{A double framed box}}&\\ovalbox{\\text{An oval framed box}}\\cr";
|
||||
// latex += "\\end{array}";
|
||||
static {
|
||||
String latex = "\\begin{array}{cc}";
|
||||
latex += "\\fbox{\\text{A framed box with \\textdbend}}&\\shadowbox{\\text{A shadowed box}}\\cr";
|
||||
latex += "\\doublebox{\\text{A double framed box}}&\\ovalbox{\\text{An oval framed box}}\\cr";
|
||||
latex += "\\end{array}";
|
||||
LATEX_BOXES = latex;
|
||||
}
|
||||
|
||||
final String markdown = "# Example of LaTeX\n\n$$"
|
||||
+ latex + "$$\n\n something like **this**";
|
||||
private TextView textView;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MenuOptions menuOptions() {
|
||||
return MenuOptions.create()
|
||||
.add("array", this::array)
|
||||
.add("longDivision", this::longDivision)
|
||||
.add("bangle", this::bangle)
|
||||
.add("boxes", this::boxes)
|
||||
.add("insideBlockQuote", this::insideBlockQuote)
|
||||
.add("legacy", this::legacy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_text_view);
|
||||
|
||||
textView = findViewById(R.id.text_view);
|
||||
|
||||
// array();
|
||||
longDivision();
|
||||
}
|
||||
|
||||
private void array() {
|
||||
render(wrapLatexInSampleMarkdown(LATEX_ARRAY));
|
||||
}
|
||||
|
||||
private void longDivision() {
|
||||
render(wrapLatexInSampleMarkdown(LATEX_LONG_DIVISION));
|
||||
}
|
||||
|
||||
private void bangle() {
|
||||
render(wrapLatexInSampleMarkdown(LATEX_BANGLE));
|
||||
}
|
||||
|
||||
private void boxes() {
|
||||
render(wrapLatexInSampleMarkdown(LATEX_BOXES));
|
||||
}
|
||||
|
||||
private void insideBlockQuote() {
|
||||
String latex = "W=W_1+W_2=F_1X_1-F_2X_2";
|
||||
final String md = "" +
|
||||
"# LaTeX inside a blockquote\n" +
|
||||
"> $$" + latex + "$$\n";
|
||||
render(md);
|
||||
}
|
||||
|
||||
private void legacy() {
|
||||
final String md = wrapLatexInSampleMarkdown(LATEX_BANGLE);
|
||||
|
||||
final Markwon markwon = Markwon.builder(this)
|
||||
// .usePlugin(JLatexMathPlugin.create(textView.getTextSize(), new JLatexMathPlugin.BuilderConfigure() {
|
||||
// @Override
|
||||
// public void configureBuilder(@NonNull JLatexMathPlugin.Builder builder) {
|
||||
// builder
|
||||
// .backgroundProvider(new JLatexMathPlugin.BackgroundProvider() {
|
||||
// @NonNull
|
||||
// @Override
|
||||
// public Drawable provide() {
|
||||
// return new ColorDrawable(0x40ff0000);
|
||||
// }
|
||||
// })
|
||||
// .fitCanvas(true)
|
||||
// .align(JLatexMathDrawable.ALIGN_LEFT)
|
||||
// .padding(48)
|
||||
// ;
|
||||
// }
|
||||
// }))
|
||||
.usePlugin(JLatexMathPlugin.create(textView.getTextSize()))
|
||||
// LEGACY does not require inline parser
|
||||
.usePlugin(JLatexMathPlugin.create(textView.getTextSize(), builder -> {
|
||||
builder.renderMode(JLatexMathPlugin.RenderMode.LEGACY);
|
||||
builder.theme()
|
||||
.backgroundProvider(() -> new ColorDrawable(0x100000ff))
|
||||
.padding(JLatexMathTheme.Padding.all(48));
|
||||
}))
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, md);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String wrapLatexInSampleMarkdown(@NonNull String latex) {
|
||||
return "" +
|
||||
"# Example of LaTeX\n\n" +
|
||||
"(inline): $$" + latex + "$$ so nice, really-really really-really really-really? Now, (block):\n\n" +
|
||||
"$$\n" +
|
||||
"" + latex + "\n" +
|
||||
"$$\n\n" +
|
||||
"the end";
|
||||
}
|
||||
|
||||
private void render(@NonNull String markdown) {
|
||||
|
||||
final float textSize = textView.getTextSize();
|
||||
final Resources r = getResources();
|
||||
|
||||
final Markwon markwon = Markwon.builder(this)
|
||||
// NB! `MarkwonInlineParserPlugin` is required in order to parse inlines
|
||||
.usePlugin(MarkwonInlineParserPlugin.create())
|
||||
.usePlugin(JLatexMathPlugin.create(textSize, textSize * 1.25F, builder -> {
|
||||
builder.theme()
|
||||
.inlineBackgroundProvider(() -> new ColorDrawable(0x1000ff00))
|
||||
.blockBackgroundProvider(() -> new ColorDrawable(0x10ff0000))
|
||||
.blockPadding(JLatexMathTheme.Padding.symmetric(
|
||||
r.getDimensionPixelSize(R.dimen.latex_block_padding_vertical),
|
||||
r.getDimensionPixelSize(R.dimen.latex_block_padding_horizontal)
|
||||
));
|
||||
|
||||
// explicitly request LEGACY rendering mode
|
||||
// builder.renderMode(JLatexMathPlugin.RenderMode.LEGACY);
|
||||
}))
|
||||
.build();
|
||||
//
|
||||
// if (true) {
|
||||
//// final String l = "$$\n" +
|
||||
//// " P(X=r)=\\frac{\\lambda^r e^{-\\lambda}}{r!}\n" +
|
||||
//// "$$\n" +
|
||||
//// "\n" +
|
||||
//// "$$\n" +
|
||||
//// " P(X<r)=P(X<r-1)\n" +
|
||||
//// "$$\n" +
|
||||
//// "\n" +
|
||||
//// "$$\n" +
|
||||
//// " P(X>r)=1-P(X<r=1)\n" +
|
||||
//// "$$\n" +
|
||||
//// "\n" +
|
||||
//// "$$\n" +
|
||||
//// " \\text{Variance} = \\lambda\n" +
|
||||
//// "$$";
|
||||
// final String l = "$$ \n" +
|
||||
// " \\sigma_T^2 = \\frac{1-p}{p^2}\n" +
|
||||
// "$$";
|
||||
// markwon.setMarkdown(textView, l);
|
||||
// return;
|
||||
// }
|
||||
|
||||
markwon.setMarkdown(textView, markdown);
|
||||
}
|
||||
|
@ -0,0 +1,169 @@
|
||||
package io.noties.markwon.sample.tasklist;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import io.noties.debug.Debug;
|
||||
import io.noties.markwon.AbstractMarkwonPlugin;
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.MarkwonSpansFactory;
|
||||
import io.noties.markwon.SpanFactory;
|
||||
import io.noties.markwon.ext.tasklist.TaskListItem;
|
||||
import io.noties.markwon.ext.tasklist.TaskListPlugin;
|
||||
import io.noties.markwon.ext.tasklist.TaskListSpan;
|
||||
import io.noties.markwon.sample.ActivityWithMenuOptions;
|
||||
import io.noties.markwon.sample.MenuOptions;
|
||||
import io.noties.markwon.sample.R;
|
||||
|
||||
public class TaskListActivity extends ActivityWithMenuOptions {
|
||||
|
||||
private static final String MD = "" +
|
||||
"- [ ] Not done here!\n" +
|
||||
"- [x] and done\n" +
|
||||
"- [X] and again!\n" +
|
||||
"* [ ] **and** syntax _included_ `code`\n" +
|
||||
"- [ ] [link](#)\n" +
|
||||
"- [ ] [a check box](https://goog.le)\n" +
|
||||
"- [x] [test]()\n" +
|
||||
"- [List](https://goog.le) 3";
|
||||
|
||||
private TextView textView;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MenuOptions menuOptions() {
|
||||
return MenuOptions.create()
|
||||
.add("regular", this::regular)
|
||||
.add("customColors", this::customColors)
|
||||
.add("customDrawableResources", this::customDrawableResources)
|
||||
.add("mutate", this::mutate);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_text_view);
|
||||
|
||||
textView = findViewById(R.id.text_view);
|
||||
|
||||
// mutate();
|
||||
regular();
|
||||
}
|
||||
|
||||
private void regular() {
|
||||
// default theme
|
||||
|
||||
final Markwon markwon = Markwon.builder(this)
|
||||
.usePlugin(TaskListPlugin.create(this))
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, MD);
|
||||
}
|
||||
|
||||
private void customColors() {
|
||||
|
||||
final int checkedFillColor = Color.RED;
|
||||
final int normalOutlineColor = Color.GREEN;
|
||||
final int checkMarkColor = Color.BLUE;
|
||||
|
||||
final Markwon markwon = Markwon.builder(this)
|
||||
.usePlugin(TaskListPlugin.create(checkedFillColor, normalOutlineColor, checkMarkColor))
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, MD);
|
||||
}
|
||||
|
||||
private void customDrawableResources() {
|
||||
// drawable **must** be stateful
|
||||
|
||||
final Drawable drawable = Objects.requireNonNull(
|
||||
ContextCompat.getDrawable(this, R.drawable.custom_task_list));
|
||||
|
||||
final Markwon markwon = Markwon.builder(this)
|
||||
.usePlugin(TaskListPlugin.create(drawable))
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, MD);
|
||||
}
|
||||
|
||||
private void mutate() {
|
||||
|
||||
final Markwon markwon = Markwon.builder(this)
|
||||
.usePlugin(TaskListPlugin.create(this))
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
// obtain origin task-list-factory
|
||||
final SpanFactory origin = builder.getFactory(TaskListItem.class);
|
||||
if (origin == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
builder.setFactory(TaskListItem.class, (configuration, props) -> {
|
||||
// maybe it's better to validate the actual type here also
|
||||
// and not force cast to task-list-span
|
||||
final TaskListSpan span = (TaskListSpan) origin.getSpans(configuration, props);
|
||||
if (span == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// NB, toggle click will intercept possible links inside task-list-item
|
||||
return new Object[]{
|
||||
span,
|
||||
new TaskListToggleSpan(span)
|
||||
};
|
||||
});
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
markwon.setMarkdown(textView, MD);
|
||||
}
|
||||
|
||||
private static class TaskListToggleSpan extends ClickableSpan {
|
||||
|
||||
private final TaskListSpan span;
|
||||
|
||||
TaskListToggleSpan(@NonNull TaskListSpan span) {
|
||||
this.span = span;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
// toggle span (this is a mere visual change)
|
||||
span.setDone(!span.isDone());
|
||||
// request visual update
|
||||
widget.invalidate();
|
||||
|
||||
// it must be a TextView
|
||||
final TextView textView = (TextView) widget;
|
||||
// it must be spanned
|
||||
final Spanned spanned = (Spanned) textView.getText();
|
||||
|
||||
// actual text of the span (this can be used along with the `span`)
|
||||
final CharSequence task = spanned.subSequence(
|
||||
spanned.getSpanStart(this),
|
||||
spanned.getSpanEnd(this)
|
||||
);
|
||||
|
||||
Debug.i("task done: %s, '%s'", span.isDone(), task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(@NonNull TextPaint ds) {
|
||||
// no op, so text is not rendered as a link
|
||||
}
|
||||
}
|
||||
}
|
5
sample/src/main/res/drawable/custom_task_list.xml
Normal file
5
sample/src/main/res/drawable/custom_task_list.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_checked="true" android:drawable="@drawable/ic_android_black_24dp" />
|
||||
<item android:drawable="@drawable/ic_home_black_36dp" />
|
||||
</selector>
|
5
sample/src/main/res/values/dimens.xml
Normal file
5
sample/src/main/res/values/dimens.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="latex_block_padding_vertical">8dip</dimen>
|
||||
<dimen name="latex_block_padding_horizontal">16dip</dimen>
|
||||
</resources>
|
@ -29,6 +29,8 @@
|
||||
|
||||
<string name="sample_inline_parser"># \# Inline Parser\n\nUsage of custom inline parser</string>
|
||||
|
||||
<string name="sample_html_details"># \# HTML <details> tag\n\n<details> tag parsed and rendered</string>
|
||||
<string name="sample_html_details"># \# HTML\n\n`details` tag parsed and rendered</string>
|
||||
|
||||
<string name="sample_task_list"># \# TaskList\n\nUsage of TaskListPlugin</string>
|
||||
|
||||
</resources>
|
Loading…
x
Reference in New Issue
Block a user