diff --git a/README.md b/README.md index 594d9781..350479bf 100644 --- a/README.md +++ b/README.md @@ -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** diff --git a/markwon-core/src/main/java/io/noties/markwon/LinkResolverDef.java b/markwon-core/src/main/java/io/noties/markwon/LinkResolverDef.java index 999dcee5..f61ad21f 100644 --- a/markwon-core/src/main/java/io/noties/markwon/LinkResolverDef.java +++ b/markwon-core/src/main/java/io/noties/markwon/LinkResolverDef.java @@ -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 + "'"); } } } diff --git a/markwon-ext-latex/build.gradle b/markwon-ext-latex/build.gradle index 6e684440..b0d3fc92 100644 --- a/markwon-ext-latex/build.gradle +++ b/markwon-ext-latex/build.gradle @@ -16,6 +16,7 @@ android { dependencies { api project(':markwon-core') + api project(':markwon-inline-parser') api deps['jlatexmath-android'] diff --git a/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexBlockImageSizeResolver.java b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexBlockImageSizeResolver.java new file mode 100644 index 00000000..7e9ac95c --- /dev/null +++ b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexBlockImageSizeResolver.java @@ -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; + } +} diff --git a/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexInlineAsyncDrawableSpan.java b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexInlineAsyncDrawableSpan.java new file mode 100644 index 00000000..10b59837 --- /dev/null +++ b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexInlineAsyncDrawableSpan.java @@ -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; + } +} diff --git a/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathBlockParser.java b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathBlockParser.java index 8f54f245..ef65bb15 100644 --- a/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathBlockParser.java +++ b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathBlockParser.java @@ -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; + } } diff --git a/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathBlockParserLegacy.java b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathBlockParserLegacy.java new file mode 100644 index 00000000..1a5ce282 --- /dev/null +++ b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathBlockParserLegacy.java @@ -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(); + } + } +} diff --git a/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathInlineProcessor.java b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathInlineProcessor.java new file mode 100644 index 00000000..84c26b4f --- /dev/null +++ b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathInlineProcessor.java @@ -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; + } +} diff --git a/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathNode.java b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathNode.java new file mode 100644 index 00000000..db7029a9 --- /dev/null +++ b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathNode.java @@ -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; + } +} diff --git a/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathPlugin.java b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathPlugin.java index 5d136ece..5a3d70b9 100644 --- a/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathPlugin.java +++ b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathPlugin.java @@ -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 { + /** + * LEGACY 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 start the block. + */ + 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() { + @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(); } } } diff --git a/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathTheme.java b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathTheme.java new file mode 100644 index 00000000..8a1d8801 --- /dev/null +++ b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatexMathTheme.java @@ -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 inline LaTeX + * @see #blockTextSize() + */ + @Px + public abstract float inlineTextSize(); + + /** + * @return text size in pixels for block LaTeX + * @see #inlineTextSize() + */ + @Px + public abstract float blockTextSize(); + + @Nullable + public abstract BackgroundProvider inlineBackgroundProvider(); + + @Nullable + public abstract BackgroundProvider blockBackgroundProvider(); + + /** + * @return boolean if block LaTeX must fit the width of canvas + */ + public abstract boolean blockFitCanvas(); + + /** + * @return horizontal alignment of block LaTeX 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; + } + } +} diff --git a/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatextAsyncDrawable.java b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatextAsyncDrawable.java new file mode 100644 index 00000000..4376d636 --- /dev/null +++ b/markwon-ext-latex/src/main/java/io/noties/markwon/ext/latex/JLatextAsyncDrawable.java @@ -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; + } +} diff --git a/markwon-inline-parser/build.gradle b/markwon-inline-parser/build.gradle index 703a18ff..32a45d7c 100644 --- a/markwon-inline-parser/build.gradle +++ b/markwon-inline-parser/build.gradle @@ -14,6 +14,7 @@ android { } dependencies { + api project(':markwon-core') api deps['x-annotations'] api deps['commonmark'] diff --git a/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/MarkwonInlineParserPlugin.java b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/MarkwonInlineParserPlugin.java new file mode 100644 index 00000000..ce80501b --- /dev/null +++ b/markwon-inline-parser/src/main/java/io/noties/markwon/inlineparser/MarkwonInlineParserPlugin.java @@ -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 { + void configureBuilder(@NonNull B factoryBuilder); + } + + @NonNull + public static MarkwonInlineParserPlugin create() { + return create(MarkwonInlineParser.factoryBuilder()); + } + + @NonNull + public static MarkwonInlineParserPlugin create(@NonNull BuilderConfigure 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 MarkwonInlineParserPlugin create( + @NonNull B factoryBuilder, + @NonNull BuilderConfigure 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; + } +} diff --git a/sample/build.gradle b/sample/build.gradle index d2a9e27f..595fd54b 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -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'] diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 5e0ae714..0c02f47f 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -35,6 +35,7 @@ + diff --git a/sample/src/main/java/io/noties/markwon/sample/ActivityWithMenuOptions.java b/sample/src/main/java/io/noties/markwon/sample/ActivityWithMenuOptions.java new file mode 100644 index 00000000..54f5342f --- /dev/null +++ b/sample/src/main/java/io/noties/markwon/sample/ActivityWithMenuOptions.java @@ -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; + } +} diff --git a/sample/src/main/java/io/noties/markwon/sample/MainActivity.java b/sample/src/main/java/io/noties/markwon/sample/MainActivity.java index 4cdd6d73..a14a8183 100644 --- a/sample/src/main/java/io/noties/markwon/sample/MainActivity.java +++ b/sample/src/main/java/io/noties/markwon/sample/MainActivity.java @@ -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); } diff --git a/sample/src/main/java/io/noties/markwon/sample/MenuOptions.java b/sample/src/main/java/io/noties/markwon/sample/MenuOptions.java new file mode 100644 index 00000000..6fb5b310 --- /dev/null +++ b/sample/src/main/java/io/noties/markwon/sample/MenuOptions.java @@ -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 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; + } +} diff --git a/sample/src/main/java/io/noties/markwon/sample/Sample.java b/sample/src/main/java/io/noties/markwon/sample/Sample.java index 36b13cd2..f243c0ec 100644 --- a/sample/src/main/java/io/noties/markwon/sample/Sample.java +++ b/sample/src/main/java/io/noties/markwon/sample/Sample.java @@ -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; diff --git a/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java b/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java index 5553c9f8..e1181a7f 100644 --- a/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java +++ b/sample/src/main/java/io/noties/markwon/sample/editor/EditorActivity.java @@ -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 diff --git a/sample/src/main/java/io/noties/markwon/sample/editor/LinkEditHandler.java b/sample/src/main/java/io/noties/markwon/sample/editor/LinkEditHandler.java index 743428d0..3a6d60fd 100644 --- a/sample/src/main/java/io/noties/markwon/sample/editor/LinkEditHandler.java +++ b/sample/src/main/java/io/noties/markwon/sample/editor/LinkEditHandler.java @@ -40,24 +40,28 @@ class LinkEditHandler extends AbstractEditHandler { 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 diff --git a/sample/src/main/java/io/noties/markwon/sample/latex/LatexActivity.java b/sample/src/main/java/io/noties/markwon/sample/latex/LatexActivity.java index 44d9aac3..8dcdcd11 100644 --- a/sample/src/main/java/io/noties/markwon/sample/latex/LatexActivity.java +++ b/sample/src/main/java/io/noties/markwon/sample/latex/LatexActivity.java @@ -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(Xr)=1-P(X { + // 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 + } + } +} diff --git a/sample/src/main/res/drawable/custom_task_list.xml b/sample/src/main/res/drawable/custom_task_list.xml new file mode 100644 index 00000000..43c2e2a8 --- /dev/null +++ b/sample/src/main/res/drawable/custom_task_list.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/sample/src/main/res/values/dimens.xml b/sample/src/main/res/values/dimens.xml new file mode 100644 index 00000000..b88d4ed5 --- /dev/null +++ b/sample/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 8dip + 16dip + \ No newline at end of file diff --git a/sample/src/main/res/values/strings-samples.xml b/sample/src/main/res/values/strings-samples.xml index d87585fd..d7f11e1a 100644 --- a/sample/src/main/res/values/strings-samples.xml +++ b/sample/src/main/res/values/strings-samples.xml @@ -29,6 +29,8 @@ # \# Inline Parser\n\nUsage of custom inline parser - # \# HTML <details> tag\n\n<details> tag parsed and rendered + # \# HTML\n\n`details` tag parsed and rendered + + # \# TaskList\n\nUsage of TaskListPlugin \ No newline at end of file