diff --git a/README.md b/README.md
index 594d9781..350479bf 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,8 @@
[](https://github.com/noties/Markwon/actions)
+
+
**Markwon** is a markdown library for Android. It parses markdown
following [commonmark-spec] with the help of amazing [commonmark-java]
library and renders result as _Android-native_ Spannables. **No HTML**
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