From db660d2a4026b963f056c5cea9df4ac695b1d6bc Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sat, 7 Mar 2020 14:14:10 +0300 Subject: [PATCH] latex block parsing tests --- CHANGELOG.md | 1 + .../markwon/ext/latex/JLatexMathPlugin.java | 15 +- .../ext/latex/JLatexMathBlockParserTest.java | 173 ++++++++++++++++++ .../ext/latex/JLatexMathPluginTest.java | 109 +++++++++++ .../basicplugins/BasicPluginsActivity.java | 72 +++++++- 5 files changed, 362 insertions(+), 8 deletions(-) create mode 100644 markwon-ext-latex/src/test/java/io/noties/markwon/ext/latex/JLatexMathBlockParserTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index b6983a78..f7410eca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * add `JLatexMathPlugin.ErrorHandler` to catch latex rendering errors and (optionally) display error drawable ([#204]) * add `SoftBreakAddsNewLinePlugin` plugin (`core` module) * `LinkResolverDef` defaults to `https` when a link does not have scheme information ([#75]) +* add `option` abstraction for `sample` module allowing switching of multiple cases in runtime via menu [#75]: https://github.com/noties/Markwon/issues/75 [#204]: https://github.com/noties/Markwon/issues/204 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 11207041..02bb618e 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 @@ -133,18 +133,19 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin { return new Builder(JLatexMathTheme.builder(inlineTextSize, blockTextSize)); } - public static class Config { + @VisibleForTesting + static class Config { // @since 4.3.0-SNAPSHOT - private final JLatexMathTheme theme; + final JLatexMathTheme theme; // @since 4.3.0-SNAPSHOT - private final RenderMode renderMode; + final RenderMode renderMode; // @since 4.3.0-SNAPSHOT - private final ErrorHandler errorHandler; + final ErrorHandler errorHandler; - private final ExecutorService executorService; + final ExecutorService executorService; Config(@NonNull Builder builder) { this.theme = builder.theme.build(); @@ -159,7 +160,9 @@ public class JLatexMathPlugin extends AbstractMarkwonPlugin { } } - private final Config config; + @VisibleForTesting + final Config config; + private final JLatextAsyncDrawableLoader jLatextAsyncDrawableLoader; private final JLatexBlockImageSizeResolver jLatexBlockImageSizeResolver; private final ImageSizeResolver inlineImageSizeResolver; diff --git a/markwon-ext-latex/src/test/java/io/noties/markwon/ext/latex/JLatexMathBlockParserTest.java b/markwon-ext-latex/src/test/java/io/noties/markwon/ext/latex/JLatexMathBlockParserTest.java new file mode 100644 index 00000000..b1587e1d --- /dev/null +++ b/markwon-ext-latex/src/test/java/io/noties/markwon/ext/latex/JLatexMathBlockParserTest.java @@ -0,0 +1,173 @@ +package io.noties.markwon.ext.latex; + +import androidx.annotation.NonNull; + +import org.commonmark.internal.BlockContinueImpl; +import org.commonmark.internal.BlockStartImpl; +import org.commonmark.internal.util.Parsing; +import org.commonmark.parser.block.BlockStart; +import org.commonmark.parser.block.ParserState; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class JLatexMathBlockParserTest { + + private static final String[] NO_MATCH = { + " ", + " ", + " ", + "$ ", + " $ $", + "-$$", + " -$$", + "$$-", + " $$ -", + " $$ -", + "$$$ -" + }; + + private static final String[] MATCH = { + "$$", + " $$", + " $$", + " $$", + "$$ ", + " $$ ", + " $$ ", + " $$ ", + "$$$", + " $$$", + " $$$", + "$$$$", + " $$$$", + "$$$$$$$$$$$$$$$$$$$$$", + " $$$$$$$$$$$$$$$$$$$$$", + " $$$$$$$$$$$$$$$$$$$$$", + " $$$$$$$$$$$$$$$$$$$$$" + }; + + private JLatexMathBlockParser.Factory factory; + + @Before + public void before() { + factory = new JLatexMathBlockParser.Factory(); + } + + @Test + public void factory_indentBlock() { + // when state indent is greater than block -> nono + + final ParserState state = mock(ParserState.class); + when(state.getIndent()).thenReturn(Parsing.CODE_BLOCK_INDENT); + + // hm, interesting, `BlockStart.none()` actually returns null + final BlockStart start = factory.tryStart(state, null); + assertNull(start); + } + + @Test + public void factory_noMatch() { + + for (String line : NO_MATCH) { + final ParserState state = createState(line); + + assertNull(factory.tryStart(state, null)); + } + } + + @Test + public void factory_match() { + + for (String line : MATCH) { + final ParserState state = createState(line); + + final BlockStart start = factory.tryStart(state, null); + assertNotNull(start); + + // hm... + final BlockStartImpl impl = (BlockStartImpl) start; + assertEquals(quote(line), line.length() + 1, impl.getNewIndex()); + } + } + + @Test + public void finish() { + + for (String line : MATCH) { + final ParserState state = createState(line); + + // we will have 2 checks here: + // * must pass for correct length + // * must fail for incorrect + + final int count = countDollarSigns(line); + + // pass + { + final JLatexMathBlockParser parser = new JLatexMathBlockParser(count); + final BlockContinueImpl impl = (BlockContinueImpl) parser.tryContinue(state); + assertTrue(quote(line), impl.isFinalize()); + } + + // fail (in terms of closing, not failing test) + { + final JLatexMathBlockParser parser = new JLatexMathBlockParser(count + 1); + final BlockContinueImpl impl = (BlockContinueImpl) parser.tryContinue(state); + assertFalse(quote(line), impl.isFinalize()); + } + } + } + + @Test + public void finish_noMatch() { + for (String line : NO_MATCH) { + final ParserState state = createState(line); + // doesn't matter + final int count = 2; + final JLatexMathBlockParser parser = new JLatexMathBlockParser(count); + final BlockContinueImpl impl = (BlockContinueImpl) parser.tryContinue(state); + assertFalse(quote(line), impl.isFinalize()); + } + } + + @NonNull + private static ParserState createState(@NonNull String line) { + + final ParserState state = mock(ParserState.class); + + int i = 0; + for (int length = line.length(); i < length; i++) { + if (' ' != line.charAt(i)) { + // previous is the last space + break; + } + } + + when(state.getIndent()).thenReturn(i); + when(state.getNextNonSpaceIndex()).thenReturn(i); + when(state.getLine()).thenReturn(line); + + return state; + } + + private static int countDollarSigns(@NonNull String line) { + int count = 0; + for (int i = 0, length = line.length(); i < length; i++) { + if ('$' == line.charAt(i)) count += 1; + } + return count; + } + + @NonNull + private static String quote(@NonNull String s) { + return '\'' + s + '\''; + } +} \ No newline at end of file diff --git a/markwon-ext-latex/src/test/java/io/noties/markwon/ext/latex/JLatexMathPluginTest.java b/markwon-ext-latex/src/test/java/io/noties/markwon/ext/latex/JLatexMathPluginTest.java index 8190a470..57584168 100644 --- a/markwon-ext-latex/src/test/java/io/noties/markwon/ext/latex/JLatexMathPluginTest.java +++ b/markwon-ext-latex/src/test/java/io/noties/markwon/ext/latex/JLatexMathPluginTest.java @@ -10,17 +10,24 @@ import org.mockito.ArgumentCaptor; import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import java.util.List; import java.util.concurrent.ExecutorService; import io.noties.markwon.MarkwonConfiguration; +import io.noties.markwon.MarkwonPlugin; import io.noties.markwon.MarkwonVisitor; import io.noties.markwon.SpannableBuilder; +import io.noties.markwon.inlineparser.InlineProcessor; +import io.noties.markwon.inlineparser.MarkwonInlineParser; +import io.noties.markwon.inlineparser.MarkwonInlineParserPlugin; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -110,4 +117,106 @@ public class JLatexMathPluginTest { verify(visitor, times(1)).setSpans(eq(0), any()); } + + @Test + public void legacy() { + // if render mode is legacy: + // - no inline plugin is required, + // - parser has legacy block parser factory + // - no inline node is registered (node) + + final JLatexMathPlugin plugin = JLatexMathPlugin.create(1, new JLatexMathPlugin.BuilderConfigure() { + @Override + public void configureBuilder(@NonNull JLatexMathPlugin.Builder builder) { + builder.renderMode(JLatexMathPlugin.RenderMode.LEGACY); + } + }); + + // registry + { + final MarkwonPlugin.Registry registry = mock(MarkwonPlugin.Registry.class); + plugin.configure(registry); + verify(registry, never()).require(any(Class.class)); + } + + // parser + { + final Parser.Builder builder = mock(Parser.Builder.class); + plugin.configureParser(builder); + + final ArgumentCaptor captor = + ArgumentCaptor.forClass(BlockParserFactory.class); + verify(builder, times(1)).customBlockParserFactory(captor.capture()); + final BlockParserFactory factory = captor.getValue(); + assertTrue(factory.getClass().getName(), factory instanceof JLatexMathBlockParserLegacy.Factory); + } + + // visitor + { + final MarkwonVisitor.Builder builder = mock(MarkwonVisitor.Builder.class); + plugin.configureVisitor(builder); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); + verify(builder, times(1)).on(captor.capture(), any(MarkwonVisitor.NodeVisitor.class)); + + assertEquals(JLatexMathBlock.class, captor.getValue()); + } + } + + @Test + public void blocks_inlines_implicit() { + final JLatexMathPlugin plugin = JLatexMathPlugin.create(1); + final JLatexMathPlugin.Config config = plugin.config; + assertEquals(JLatexMathPlugin.RenderMode.BLOCKS_AND_INLINES, config.renderMode); + } + + @Test + public void blocks_inlines() { + final JLatexMathPlugin plugin = JLatexMathPlugin.create(12); + + // registry + { + final MarkwonInlineParser.FactoryBuilder factoryBuilder = mock(MarkwonInlineParser.FactoryBuilder.class); + final MarkwonInlineParserPlugin inlineParserPlugin = mock(MarkwonInlineParserPlugin.class); + final MarkwonPlugin.Registry registry = mock(MarkwonPlugin.Registry.class); + when(inlineParserPlugin.factoryBuilder()).thenReturn(factoryBuilder); + when(registry.require(eq(MarkwonInlineParserPlugin.class))).thenReturn(inlineParserPlugin); + plugin.configure(registry); + + verify(registry, times(1)).require(eq(MarkwonInlineParserPlugin.class)); + verify(inlineParserPlugin, times(1)).factoryBuilder(); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(InlineProcessor.class); + verify(factoryBuilder, times(1)).addInlineProcessor(captor.capture()); + + final InlineProcessor inlineProcessor = captor.getValue(); + assertTrue(inlineParserPlugin.getClass().getName(), inlineProcessor instanceof JLatexMathInlineProcessor); + } + + // parser + { + final Parser.Builder builder = mock(Parser.Builder.class); + plugin.configureParser(builder); + + final ArgumentCaptor captor = + ArgumentCaptor.forClass(BlockParserFactory.class); + verify(builder, times(1)).customBlockParserFactory(captor.capture()); + final BlockParserFactory factory = captor.getValue(); + assertTrue(factory.getClass().getName(), factory instanceof JLatexMathBlockParser.Factory); + } + + // visitor + { + final MarkwonVisitor.Builder builder = mock(MarkwonVisitor.Builder.class); + plugin.configureVisitor(builder); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); + verify(builder, times(2)).on(captor.capture(), any(MarkwonVisitor.NodeVisitor.class)); + + final List nodes = captor.getAllValues(); + assertEquals(2, nodes.size()); + assertTrue(nodes.toString(), nodes.contains(JLatexMathNode.class)); + assertTrue(nodes.toString(), nodes.contains(JLatexMathBlock.class)); + } + } } \ No newline at end of file diff --git a/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java b/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java index 49703e72..02878480 100644 --- a/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java +++ b/sample/src/main/java/io/noties/markwon/sample/basicplugins/BasicPluginsActivity.java @@ -21,8 +21,12 @@ import io.noties.markwon.Markwon; import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonSpansFactory; import io.noties.markwon.MarkwonVisitor; +import io.noties.markwon.RenderProps; import io.noties.markwon.SoftBreakAddsNewLinePlugin; +import io.noties.markwon.SpanFactory; +import io.noties.markwon.core.CoreProps; import io.noties.markwon.core.MarkwonTheme; +import io.noties.markwon.core.spans.LastLineSpacingSpan; import io.noties.markwon.image.ImageItem; import io.noties.markwon.image.ImagesPlugin; import io.noties.markwon.image.SchemeHandler; @@ -46,7 +50,9 @@ public class BasicPluginsActivity extends ActivityWithMenuOptions { .add("linkWithMovementMethod", this::linkWithMovementMethod) .add("imagesPlugin", this::imagesPlugin) .add("softBreakAddsSpace", this::softBreakAddsSpace) - .add("softBreakAddsNewLine", this::softBreakAddsNewLine); + .add("softBreakAddsNewLine", this::softBreakAddsNewLine) + .add("additionalSpacing", this::additionalSpacing) + .add("headingNoSpace", this::headingNoSpace); } @Override @@ -242,6 +248,69 @@ public class BasicPluginsActivity extends ActivityWithMenuOptions { markwon.setMarkdown(textView, md); } + private void additionalSpacing() { + + // please note that bottom line (after 1 & 2 levels) will be drawn _AFTER_ padding + final int spacing = (int) (128 * getResources().getDisplayMetrics().density + .5F); + + final Markwon markwon = Markwon.builder(this) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureTheme(@NonNull MarkwonTheme.Builder builder) { + builder.headingBreakHeight(0); + } + + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + builder.appendFactory( + Heading.class, + (configuration, props) -> new LastLineSpacingSpan(spacing)); + } + }) + .build(); + + final String md = "" + + "# Title title title title title title title title title title \n\ntext text text text"; + + markwon.setMarkdown(textView, md); + } + + private void headingNoSpace() { + final Markwon markwon = Markwon.builder(this) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureTheme(@NonNull MarkwonTheme.Builder builder) { + builder.headingBreakHeight(0); + } + + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + builder.on(Heading.class, (visitor, heading) -> { + + visitor.ensureNewLine(); + + final int length = visitor.length(); + visitor.visitChildren(heading); + + CoreProps.HEADING_LEVEL.set(visitor.renderProps(), heading.getLevel()); + + visitor.setSpansForNodeOptional(heading, length); + + if (visitor.hasNext(heading)) { + visitor.ensureNewLine(); +// visitor.forceNewLine(); + } + }); + } + }) + .build(); + + final String md = "" + + "# Title title title title title title title title title title \n\ntext text text text"; + + markwon.setMarkdown(textView, md); + } + // public void step_6() { // // final Markwon markwon = Markwon.builder(this) @@ -270,5 +339,4 @@ public class BasicPluginsActivity extends ActivityWithMenuOptions { // rendering lifecycle (before/after) // renderProps // process - // priority }