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