> 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);
}
}