> 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("").append(tag).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
+ );
+ }
+}
diff --git a/java/md2html/Pre.java b/java/md2html/Pre.java
new file mode 100644
index 0000000..d191ee4
--- /dev/null
+++ b/java/md2html/Pre.java
@@ -0,0 +1,11 @@
+package md2html;
+
+import java.util.List;
+import markup.*;
+
+public class Pre extends AbstractMarkup implements PartOfParagraph {
+
+ public Pre(List items) {
+ super(items, "```", "pre", "", "");
+ }
+}
diff --git a/java/md2html/package-info.java b/java/md2html/package-info.java
new file mode 100644
index 0000000..ed6c2c3
--- /dev/null
+++ b/java/md2html/package-info.java
@@ -0,0 +1,8 @@
+/**
+ * Tests for Markdown to HTML homework
+ * of Introduction to Programming course.
+ *
+ *
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+package md2html;