package md2html; import base.*; import java.util.*; /** * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) */ public class Md2HtmlTester { private static final Map ESCAPES = Map.of("<", "<", ">", ">"); private final Map elements = new HashMap<>(); private final Map> tags = new LinkedHashMap<>(); private final List> tests = new ArrayList<>(); public Md2HtmlTester() { addElement("em", "*"); addElement("em", "_"); addElement("strong", "**"); addElement("strong", "__"); addElement("s", "--"); addElement("code", "`"); test( "# Заголовок первого уровня\n\n", "

Заголовок первого уровня

" ); test( "## Второго\n\n", "

Второго

" ); test( "### Третьего ## уровня\n\n", "

Третьего ## уровня

" ); test( "#### Четвертого\n# Все еще четвертого\n\n", "

Четвертого\n# Все еще четвертого

" ); test( "Этот абзац текста\nсодержит две строки.", "

Этот абзац текста\nсодержит две строки.

" ); test( " # Может показаться, что это заголовок.\nНо нет, это абзац, начинающийся с `#`.\n\n", "

# Может показаться, что это заголовок.\nНо нет, это абзац, начинающийся с #.

" ); test( "#И это не заголовок.\n\n", "

#И это не заголовок.

" ); test( "###### Заголовки могут быть многострочными\n(и с пропуском заголовков предыдущих уровней)\n\n", "
Заголовки могут быть многострочными\n(и с пропуском заголовков предыдущих уровней)
" ); test( "Мы все любим *выделять* текст _разными_ способами.\n**Сильное выделение**, используется гораздо реже,\nно __почему бы и нет__?\nНемного --зачеркивания-- еще никому не вредило.\nКод представляется элементом `code`.\n\n", "

Мы все любим выделять текст разными способами.\nСильное выделение, используется гораздо реже,\nно почему бы и нет?\nНемного зачеркивания еще никому не вредило.\nКод представляется элементом code.

" ); test( "Обратите внимание, как экранируются специальные\nHTML-символы, такие как `<`, `>` и `&`.\n\n", "

Обратите внимание, как экранируются специальные\nHTML-символы, такие как <, > и &.

" ); test( "Экранирование должно работать во всех местах: <>&.\n\n", "

Экранирование должно работать во всех местах: <>&.

" ); test( "Знаете ли вы, что в Markdown, одиночные * и _\nне означают выделение?\nОни так же могут быть заэкранированы\nпри помощи обратного слэша: \\*.", "

Знаете ли вы, что в Markdown, одиночные * и _\nне означают выделение?\nОни так же могут быть заэкранированы\nпри помощи обратного слэша: *.

" ); test( "\n\n\nЛишние пустые строки должны игнорироваться.\n\n\n\n", "

Лишние пустые строки должны игнорироваться.

" ); test( "Любите ли вы *вложенные __выделения__* так,\nкак __--люблю--__ их я?", "

Любите ли вы вложенные выделения так,\nкак люблю их я?

" ); test( """ # Заголовок первого уровня ## Второго ### Третьего ## уровня #### Четвертого # Все еще четвертого Этот абзац текста содержит две строки. # Может показаться, что это заголовок. Но нет, это абзац, начинающийся с `#`. #И это не заголовок. ###### Заголовки могут быть многострочными (и с пропуском заголовков предыдущих уровней) Мы все любим *выделять* текст _разными_ способами. **Сильное выделение**, используется гораздо реже, но __почему бы и нет__? Немного --зачеркивания-- еще никому не вредило. Код представляется элементом `code`. Обратите внимание, как экранируются специальные HTML-символы, такие как `<`, `>` и `&`. Знаете ли вы, что в Markdown, одиночные * и _ не означают выделение? Они так же могут быть заэкранированы при помощи обратного слэша: \\*. Лишние пустые строки должны игнорироваться. Любите ли вы *вложенные __выделения__* так, как __--люблю--__ их я? """, """

Заголовок первого уровня

Второго

Третьего ## уровня

Четвертого # Все еще четвертого

Этот абзац текста содержит две строки.

# Может показаться, что это заголовок. Но нет, это абзац, начинающийся с #.

#И это не заголовок.

Заголовки могут быть многострочными (и с пропуском заголовков предыдущих уровней)

Мы все любим выделять текст разными способами. Сильное выделение, используется гораздо реже, но почему бы и нет? Немного зачеркивания еще никому не вредило. Код представляется элементом code.

Обратите внимание, как экранируются специальные HTML-символы, такие как <, > и &.

Знаете ли вы, что в Markdown, одиночные * и _ не означают выделение? Они так же могут быть заэкранированы при помощи обратного слэша: *.

Лишние пустые строки должны игнорироваться.

Любите ли вы вложенные выделения так, как люблю их я?

""" ); test("# Без перевода строки в конце", "

Без перевода строки в конце

"); test("# Один перевод строки в конце\n", "

Один перевод строки в конце

"); test("# Два перевода строки в конце\n\n", "

Два перевода строки в конце

"); test( "Выделение может *начинаться на одной строке,\n а заканчиваться* на другой", "

Выделение может начинаться на одной строке,\n а заканчиваться на другой

" ); test("# *Выделение* и `код` в заголовках", "

Выделение и код в заголовках

"); } protected void addElement(final String tag, final String markup) { addElement(tag, markup, markup); } protected void addElement(final String tag, final String begin, final String end) { addElement(tag, begin, (checker, markup, input, output) -> { checker.space(input, output); input.append(begin); open(output, tag); checker.word(input, output); checker.generate(markup, input, output); checker.word(input, output); input.append(end); close(output, tag); checker.space(input, output); }); } public void addElement(final String tag, final String begin, final Generator generator) { Asserts.assertTrue("Duplicate element " + begin, elements.put(begin, generator) == null); tags.computeIfAbsent(tag, k -> new ArrayList<>()).add(begin); } private final Runner runner = Runner.packages("md2html").files("Md2Html"); protected Md2HtmlTester test(final String input, final String output) { tests.add(Pair.of(input, output)); return this; } protected Md2HtmlTester spoiled(final String input, final String output, final String... spoilers) { for (final String spoiler : spoilers) { final Indexer in = new Indexer(input, spoiler); final Indexer out = new Indexer(output, ESCAPES.getOrDefault(spoiler, spoiler)); while (in.next() && out.next()) { tests.add(Pair.of(in.cut(), out.cut())); tests.add(Pair.of(in.escape(), output)); } } return this; } private static class Indexer { private final String string; private final String spoiler; private int index = - 1; public Indexer(final String string, final String spoiler) { this.string = string; this.spoiler = spoiler; } public boolean next() { index = string.indexOf(spoiler, index + 1); return index >= 0; } public String cut() { return string.substring(0, index) + string.substring(index + spoiler.length()); } public String escape() { return string.substring(0, index) + "\\" + string.substring(index); } } private static void open(final StringBuilder output, final String tag) { output.append("<").append(tag).append(">"); } private static void close(final StringBuilder output, final String tag) { output.append(""); } public void test(final TestCounter counter) { counter.scope("Testing " + String.join(", ", tags.keySet()), () -> new Checker(counter).test()); } public class Checker extends BaseChecker { public Checker(final TestCounter counter) { super(counter); } protected void test() { for (final Pair test : tests) { test(test); } for (final String markup : elements.keySet()) { randomTest(3, 10, List.of(markup)); } final int d = TestCounter.DENOMINATOR; for (int i = 0; i < 10; i++) { randomTest(100, 1000, randomMarkup()); } randomTest(100, 100_000 / d, randomMarkup()); } private void test(final Pair test) { runner.testEquals(counter, Arrays.asList(test.first().split("\n")), Arrays.asList(test.second().split("\n"))); } private List randomMarkup() { return Functional.map(tags.values(), random()::randomItem); } private void randomTest(final int paragraphs, final int length, final List markup) { final StringBuilder input = new StringBuilder(); final StringBuilder output = new StringBuilder(); emptyLines(input); final List markupList = new ArrayList<>(markup); for (int i = 0; i < paragraphs; i++) { final StringBuilder inputSB = new StringBuilder(); paragraph(length, inputSB, output, markupList); input.append(inputSB); emptyLines(input); } test(Pair.of(input.toString(), output.toString())); } private void paragraph(final int length, final StringBuilder input, final StringBuilder output, final List markup) { final int h = random().nextInt(0, 6); final String tag = h == 0 ? "p" : "h" + h; if (h > 0) { input.append(new String(new char[h]).replace('\0', '#')).append(" "); } open(output, tag); while (input.length() < length) { generate(markup, input, output); final String middle = random().randomString(ExtendedRandom.ENGLISH); input.append(middle).append("\n"); output.append(middle).append("\n"); } output.setLength(output.length() - 1); close(output, tag); output.append("\n"); input.append("\n"); } private void space(final StringBuilder input, final StringBuilder output) { if (random().nextBoolean()) { final String space = random().nextBoolean() ? " " : "\n"; input.append(space); output.append(space); } } public void generate(final List markup, final StringBuilder input, final StringBuilder output) { word(input, output); if (markup.isEmpty()) { return; } final String type = random().randomItem(markup); markup.remove(type); elements.get(type).generate(this, markup, input, output); markup.add(type); } protected void word(final StringBuilder input, final StringBuilder output) { final String word = random().randomString(random().randomItem(ExtendedRandom.ENGLISH, ExtendedRandom.GREEK, ExtendedRandom.RUSSIAN)); input.append(word); output.append(word); } private void emptyLines(final StringBuilder sb) { while (random().nextBoolean()) { sb.append('\n'); } } String generateInput(final List markup) { final StringBuilder sb = new StringBuilder(); generate(markup, sb, new StringBuilder()); return sb.toString() .replace("<", "") .replace(">", "") .replace("]", ""); } } @FunctionalInterface public interface Generator { void generate(Checker checker, List markup, StringBuilder input, StringBuilder output); } }