latex block parsing tests

This commit is contained in:
Dimitry Ivanov 2020-03-07 14:14:10 +03:00
parent 5c3763a9a1
commit db660d2a40
5 changed files with 362 additions and 8 deletions

View File

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

View File

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

View File

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

View File

@ -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<BlockParserFactory> 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<Class> 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<InlineProcessor> 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<BlockParserFactory> 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<Class> captor = ArgumentCaptor.forClass(Class.class);
verify(builder, times(2)).on(captor.capture(), any(MarkwonVisitor.NodeVisitor.class));
final List<Class> nodes = captor.getAllValues();
assertEquals(2, nodes.size());
assertTrue(nodes.toString(), nodes.contains(JLatexMathNode.class));
assertTrue(nodes.toString(), nodes.contains(JLatexMathBlock.class));
}
}
}

View File

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