Files
prog-intro-2025/java/md2html/Md2HtmlTester.java
2026-01-29 23:20:12 +05:00

356 lines
17 KiB
Java
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package md2html;
import base.*;
import java.util.*;
/**
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
*/
public class Md2HtmlTester {
private static final Map<String, String> ESCAPES = Map.of("<", "&lt;", ">", "&gt;");
private final Map<String, Generator> elements = new HashMap<>();
private final Map<String, List<String>> tags = new LinkedHashMap<>();
private final List<Pair<String, String>> tests = new ArrayList<>();
public Md2HtmlTester() {
addElement("em", "*");
addElement("em", "_");
addElement("strong", "**");
addElement("strong", "__");
addElement("s", "--");
addElement("code", "`");
test(
"# Заголовок первого уровня\n\n",
"<h1>Заголовок первого уровня</h1>"
);
test(
"## Второго\n\n",
"<h2>Второго</h2>"
);
test(
"### Третьего ## уровня\n\n",
"<h3>Третьего ## уровня</h3>"
);
test(
"#### Четвертого\n# Все еще четвертого\n\n",
"<h4>Четвертого\n# Все еще четвертого</h4>"
);
test(
"Этот абзац текста\nсодержит две строки.",
"<p>Этот абзац текста\nсодержит две строки.</p>"
);
test(
" # Может показаться, что это заголовок.\nНо нет, это абзац, начинающийся с `#`.\n\n",
"<p> # Может показаться, что это заголовок.\nНо нет, это абзац, начинающийся с <code>#</code>.</p>"
);
test(
"#И это не заголовок.\n\n",
"<p>#И это не заголовок.</p>"
);
test(
"###### Заголовки могут быть многострочными\n(и с пропуском заголовков предыдущих уровней)\n\n",
"<h6>Заголовки могут быть многострочными\n(и с пропуском заголовков предыдущих уровней)</h6>"
);
test(
"Мы все любим *выделять* текст _разными_ способами.\n**Сильное выделение**, используется гораздо реже,\о __почему бы и нет__?\nНемного --зачеркивания-- еще никому не вредило.\nКод представляется элементом `code`.\n\n",
"<p>Мы все любим <em>выделять</em> текст <em>разными</em> способами.\n<strong>Сильное выделение</strong>, используется гораздо реже,\о <strong>почему бы и нет</strong>?\nНемного <s>зачеркивания</s> еще никому не вредило.\nКод представляется элементом <code>code</code>.</p>"
);
test(
"Обратите внимание, как экранируются специальные\nHTML-символы, такие как `<`, `>` и `&`.\n\n",
"<p>Обратите внимание, как экранируются специальные\nHTML-символы, такие как <code>&lt;</code>, <code>&gt;</code> и <code>&amp;</code>.</p>"
);
test(
"Экранирование должно работать во всех местах: <>&.\n\n",
"<p>Экранирование должно работать во всех местах: &lt;&gt;&amp;.</p>"
);
test(
"Знаете ли вы, что в Markdown, одиночные * и _\е означают выделение?\nОни так же могут быть заэкранированы\nпри помощи обратного слэша: \\*.",
"<p>Знаете ли вы, что в Markdown, одиночные * и _\е означают выделение?\nОни так же могут быть заэкранированы\nпри помощи обратного слэша: *.</p>"
);
test(
"\n\n\nЛишние пустые строки должны игнорироваться.\n\n\n\n",
"<p>Лишние пустые строки должны игнорироваться.</p>"
);
test(
"Любите ли вы *вложенные __выделения__* так,\ак __--люблю--__ их я?",
"<p>Любите ли вы <em>вложенные <strong>выделения</strong></em> так,\ак <strong><s>люблю</s></strong> их я?</p>"
);
test(
"""
# Заголовок первого уровня
## Второго
### Третьего ## уровня
#### Четвертого
# Все еще четвертого
Этот абзац текста
содержит две строки.
# Может показаться, что это заголовок.
Но нет, это абзац, начинающийся с `#`.
#И это не заголовок.
###### Заголовки могут быть многострочными
с пропуском заголовков предыдущих уровней)
Мы все любим *выделять* текст _разными_ способами.
**Сильное выделение**, используется гораздо реже,
но __почему бы и нет__?
Немного --зачеркивания-- еще никому не вредило.
Код представляется элементом `code`.
Обратите внимание, как экранируются специальные
HTML-символы, такие как `<`, `>` и `&`.
Знаете ли вы, что в Markdown, одиночные * и _
не означают выделение?
Они так же могут быть заэкранированы
при помощи обратного слэша: \\*.
Лишние пустые строки должны игнорироваться.
Любите ли вы *вложенные __выделения__* так,
как __--люблю--__ их я?
""",
"""
<h1>Заголовок первого уровня</h1>
<h2>Второго</h2>
<h3>Третьего ## уровня</h3>
<h4>Четвертого
# Все еще четвертого</h4>
<p>Этот абзац текста
содержит две строки.</p>
<p> # Может показаться, что это заголовок.
Но нет, это абзац, начинающийся с <code>#</code>.</p>
<p>#И это не заголовок.</p>
<h6>Заголовки могут быть многострочными
с пропуском заголовков предыдущих уровней)</h6>
<p>Мы все любим <em>выделять</em> текст <em>разными</em> способами.
<strong>Сильное выделение</strong>, используется гораздо реже,
но <strong>почему бы и нет</strong>?
Немного <s>зачеркивания</s> еще никому не вредило.
Код представляется элементом <code>code</code>.</p>
<p>Обратите внимание, как экранируются специальные
HTML-символы, такие как <code>&lt;</code>, <code>&gt;</code> и <code>&amp;</code>.</p>
<p>Знаете ли вы, что в Markdown, одиночные * и _
не означают выделение?
Они так же могут быть заэкранированы
при помощи обратного слэша: *.</p>
<p>Лишние пустые строки должны игнорироваться.</p>
<p>Любите ли вы <em>вложенные <strong>выделения</strong></em> так,
как <strong><s>люблю</s></strong> их я?</p>
"""
);
test("# Без перевода строки в конце", "<h1>Без перевода строки в конце</h1>");
test("# Один перевод строки в конце\n", "<h1>Один перевод строки в конце</h1>");
test("# Два перевода строки в конце\n\n", "<h1>Два перевода строки в конце</h1>");
test(
"Выделение может *начинаться на одной строке,\n а заканчиваться* на другой",
"<p>Выделение может <em>начинаться на одной строке,\n а заканчиваться</em> на другой</p>"
);
test("# *Выделение* и `код` в заголовках", "<h1><em>Выделение</em> и <code>код</code> в заголовках</h1>");
}
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<String, String> 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<String, String> test) {
runner.testEquals(counter, Arrays.asList(test.first().split("\n")), Arrays.asList(test.second().split("\n")));
}
private List<String> randomMarkup() {
return Functional.map(tags.values(), random()::randomItem);
}
private void randomTest(final int paragraphs, final int length, final List<String> markup) {
final StringBuilder input = new StringBuilder();
final StringBuilder output = new StringBuilder();
emptyLines(input);
final List<String> 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<String> 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<String> 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<String> 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<String> markup, StringBuilder input, StringBuilder output);
}
}