Merge branch 'f/latex-inline' into develop

This commit is contained in:
Dimitry Ivanov 2020-02-26 17:04:50 +03:00
commit f61e0b7b20
27 changed files with 1470 additions and 253 deletions

View File

@ -4,6 +4,8 @@
[![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
following [commonmark-spec] with the help of amazing [commonmark-java]
library and renders result as _Android-native_ Spannables. **No HTML**

View File

@ -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 + "'");
}
}
}

View File

@ -16,6 +16,7 @@ android {
dependencies {
api project(':markwon-core')
api project(':markwon-inline-parser')
api deps['jlatexmath-android']

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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,31 +38,28 @@ 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) {
// 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) {
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();
}
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;
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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) {
// 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,18 +207,57 @@ 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
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
@ -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());
}
// 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;
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();
}
}
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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -14,6 +14,7 @@ android {
}
dependencies {
api project(':markwon-core')
api deps['x-annotations']
api deps['commonmark']

View File

@ -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;
}
}

View File

@ -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']

View File

@ -35,6 +35,7 @@
<activity android:name=".inlineparser.InlineParserActivity" />
<activity android:name=".htmldetails.HtmlDetailsActivity" />
<activity android:name=".tasklist.TaskListActivity" />
</application>

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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

View File

@ -40,25 +40,29 @@ 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;
}
}
if (start > -1) {
editable.setSpan(
editLinkSpan,
s,
e,
start,
start + spanTextLength,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
}
@NonNull
@Override

View File

@ -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);
}

View File

@ -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
}
}
}

View 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>

View 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>

View File

@ -29,6 +29,8 @@
<string name="sample_inline_parser"># \# Inline Parser\n\nUsage of custom inline parser</string>
<string name="sample_html_details"># \# HTML &lt;details> tag\n\n&lt;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>