update
All checks were successful
Markup Tests / test (push) Successful in 8s
Markdown to Html Tests / test (push) Successful in 17s

This commit is contained in:
2026-02-17 09:32:08 +03:00
commit 2f05f238e9
109 changed files with 9369 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
package md2html;
import java.util.ArrayList;
import java.util.List;
public class BlockCreator {
private final String text;
private final List<String> blocks;
public BlockCreator(String text) {
this.text = text;
blocks = new ArrayList<>();
}
public List<String> divideByBlocks() {
StringBuilder sb = new StringBuilder();
int i = 0;
while (i < text.length()) {
if (isNewLine(text.charAt(i))) {
i = newLine(i);
if (i < text.length() && isNewLine(text.charAt(i))) {
i = newLine(i);
addToBlock(sb);
} else if (i < text.length()){
if (!sb.isEmpty()) {
sb.append(System.lineSeparator());
}
sb.append(text.charAt(i++));
}
} else {
sb.append(text.charAt(i++));
}
}
addToBlock(sb);
return blocks;
}
private void addToBlock(StringBuilder item) {
if (!item.isEmpty()) {
blocks.add(item.toString());
item.setLength(0);
}
}
private int newLine(int i) {
if (i < text.length() && text.charAt(i) == '\r') {
i++;
if (i < text.length() && text.charAt(i) == '\n') {
i++;
}
} else {
i++;
}
return i;
}
private static boolean isNewLine(char ch) {
return (ch == '\u2028') || (ch == '\u2029') ||
(ch == '\u0085') || (ch == '\n') || (ch == '\r');
}
}

11
java/md2html/Code.java Normal file
View File

@@ -0,0 +1,11 @@
package md2html;
import java.util.List;
import markup.*;
public class Code extends AbstractMarkup implements PartOfParagraph {
public Code(List<PartOfParagraph> items) {
super(items, "'", "code", "", "");
}
}

14
java/md2html/Header.java Normal file
View File

@@ -0,0 +1,14 @@
package md2html;
import java.util.List;
import markup.*;
public class Header extends AbstractMarkup implements PrimePart {
public Header(List<PartOfParagraph> items, int level) {
super(items,
"#".repeat(level),
"h" + level,
"",
"");
}
}

64
java/md2html/Md2Html.java Normal file
View File

@@ -0,0 +1,64 @@
package md2html;
import markup.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class Md2Html {
public static void main(String[] args) {
String text = readFile(args[0]);
BlockCreator creator = new BlockCreator(text);
List<String> blocks = creator.divideByBlocks();
List<PrimePart> parts = new ArrayList<>();
for (String block : blocks) {
PrimePartCreator creatorPrime = new PrimePartCreator(block);
parts.add(creatorPrime.createPart());
}
StringBuilder sb = new StringBuilder();
for (int i = 0; i < parts.size(); i++) {
parts.get(i).toHtml(sb);
if (i < parts.size() - 1) {
sb.append(System.lineSeparator());
}
}
writeToFile(args[1], sb.toString());
}
public static String readFile(String nameOfFile) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(
new FileInputStream(nameOfFile), StandardCharsets.UTF_8
)
)) {
StringBuilder text = new StringBuilder();
int read = reader.read();
while (read != -1) {
text.append((char) read);
read = reader.read();
}
return text.toString();
} catch (FileNotFoundException e) {
System.out.println("Input file not found: " + e.getMessage());
} catch (IOException e) {
System.out.println("IOException file not found: " + e.getMessage());
}
return null;
}
public static void writeToFile(String nameOfFile, String text) {
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(
new FileOutputStream(nameOfFile), StandardCharsets.UTF_8
)
)) {
writer.write(text);
} catch (FileNotFoundException e) {
System.out.println("Output file not found: " + e.getMessage());
} catch (IOException e) {
System.out.println("IOException file not found: " + e.getMessage());
}
}
}

View File

@@ -0,0 +1,71 @@
package md2html;
import base.Selector;
import java.util.function.Consumer;
/**
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
*/
public final class Md2HtmlTest {
// === 3637
private static final Consumer<? super Md2HtmlTester> INS = tester -> tester
.test("<<вставка>>", "<p><ins>вставка</ins></p>")
.test("Это <<вставка>>, вложенная в текст", "<p>Это <ins>вставка</ins>, вложенная в текст</p>")
.spoiled("Это не <<вставка>>", "<p>Это не &lt;&lt;вставка&gt;&gt;</p>", "<", ">")
.spoiled("Это не <<вставка>> 2", "<p>Это не &lt;&lt;вставка&gt;&gt; 2</p>", "<", ">")
.addElement("ins", "<<", ">>");
private static final Consumer<? super Md2HtmlTester> DEL = tester -> tester
.test("}}удаление{{", "<p><del>удаление</del></p>")
.test("Это }}удаление{{, вложенное в текст", "<p>Это <del>удаление</del>, вложенное в текст</p>")
.spoiled("Это не }}удаление{{", "<p>Это не }}удаление{{</p>", "{")
.spoiled("Это не }}удаление{{ 2", "<p>Это не }}удаление{{ 2</p>", "{")
.addElement("del", "}}", "{{");
// === 3839
private static final Consumer<? super Md2HtmlTester> PRE = tester -> tester
.test("```код __без__ форматирования```", "<p><pre>код __без__ форматирования</pre></p>")
.test(
"Это не `\\``код __без__ форматирования``\\`",
"<p>Это не <code>`</code>код <strong>без</strong> форматирования<code></code>`</p>"
)
.addElement("pre", "```", (checker, markup, input, output) -> {
final String contentS = checker.generateInput(markup).replace("`", "");
input.append("```").append(contentS).append("```");
output.append("<pre>").append(contentS.replace("<", "&lt;").replace(">", "")).append("</pre>");
});
// === 3435
private static final Consumer<? super Md2HtmlTester> SAMP = tester -> tester
.test("!!пример!!", "<p><samp>пример</samp></p>")
.test("Это !!пример!!, вложенный в текст", "<p>Это <samp>пример</samp>, вложенный в текст</p>")
.spoiled("Это не !!пример!!", "<p>Это не !!пример!!</p>", "!")
.spoiled("Это не !!пример!! 2", "<p>Это не !!пример!! 2</p>", "!")
.addElement("samp", "!!");
// === 3233
private static final Consumer<Md2HtmlTester> VAR = tester -> tester
.test("%переменная%", "<p><var>переменная</var></p>")
.test("Это %переменная%, вложенная в текст", "<p>Это <var>переменная</var>, вложенная в текст</p>")
.spoiled("Это не %переменная%", "<p>Это не %переменная%</p>", "%")
.spoiled("Это не %переменная% 2", "<p>Это не %переменная% 2</p>", "%")
.addElement("var", "%");
// === Common
public static final Selector SELECTOR = Selector.composite(Md2HtmlTest.class, c -> new Md2HtmlTester(), Md2HtmlTester::test)
.variant("Base")
.variant("3637", INS, DEL)
.variant("3839", PRE)
.variant("3435", SAMP)
.variant("3233", VAR)
.selector();
private Md2HtmlTest() {
}
public static void main(final String... args) {
SELECTOR.main(args);
}
}

View File

@@ -0,0 +1,355 @@
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);
}
}

View File

@@ -0,0 +1,253 @@
package md2html;
import java.util.ArrayList;
import java.util.List;
import markup.*;
public class PrimePartCreator {
private final String block;
private int currentChar;
public enum MarkdownToken {
WORD,
EMPHASIS_STAR,
STRONG_STAR,
EMPHASIS_UNDERLINE,
STRONG_UNDERLINE,
STRIKEOUT,
CODE,
SCREENING,
NOTHING,
}
public PrimePartCreator(String block) {
this.block = block;
currentChar = 0;
}
public PrimePart createPart() {
PrimePart result;
if (isHeader()) {
int levelOfHeader = 0;
while (
levelOfHeader < block.length() &&
block.charAt(levelOfHeader) == '#'
) {
levelOfHeader++;
}
currentChar = levelOfHeader + 1;
result = new Header(buildPart(MarkdownToken.WORD), levelOfHeader);
} else {
currentChar = 0;
result = new Paragraph(buildPart(MarkdownToken.WORD));
}
return result;
}
private List<PartOfParagraph> buildPart(MarkdownToken currentToken) {
List<PartOfParagraph> items = new ArrayList<>();
MarkdownToken token = nextMarkdownToken();
StringBuilder sb = new StringBuilder();
while (token != MarkdownToken.NOTHING) {
if (
(token == currentToken && currentToken != MarkdownToken.WORD) ||
isSuffix(currentToken)
) {
addToList(items, sb);
if (
!(token == currentToken &&
currentToken != MarkdownToken.WORD)
) {
currentChar++;
}
break;
}
switch (token) {
case STRIKEOUT -> {
addToList(items, sb);
Strikeout strikeout = new Strikeout(buildPart(token));
items.add(strikeout);
}
case STRONG_STAR, STRONG_UNDERLINE -> {
addToList(items, sb);
Strong strong = new Strong(buildPart(token));
items.add(strong);
}
case EMPHASIS_STAR, EMPHASIS_UNDERLINE -> {
addToList(items, sb);
Emphasis emphasis = new Emphasis(buildPart(token));
items.add(emphasis);
}
case SCREENING -> {
addToList(items, sb);
items.add(
new Text(String.valueOf(block.charAt(currentChar)))
);
currentChar++;
}
case CODE -> {
addToList(items, sb);
Code code = new Code(buildPart(token));
items.add(code);
}
default -> {
if (currentChar < block.length()) {
if (block.charAt(currentChar) == '<') {
sb.append("&lt;");
} else if (block.charAt(currentChar) == '>') {
sb.append("&gt;");
} else if (block.charAt(currentChar) == '&') {
sb.append("&amp;");
} else {
sb.append(block.charAt(currentChar));
}
currentChar++;
}
}
}
token = nextMarkdownToken();
}
if (token == MarkdownToken.NOTHING) {
addToList(items, sb);
}
return items;
}
private MarkdownToken nextMarkdownToken() {
if (currentChar == block.length()) {
return MarkdownToken.NOTHING;
}
switch (block.charAt(currentChar)) {
case '*' -> {
if (isPrefix("**")) {
currentChar += 2;
return MarkdownToken.STRONG_STAR;
} else if (isPrefix("*")) {
currentChar += 1;
return MarkdownToken.EMPHASIS_STAR;
}
}
case '_' -> {
if (isPrefix("__")) {
currentChar += 2;
return MarkdownToken.STRONG_UNDERLINE;
} else if (isPrefix("_")) {
currentChar += 1;
return MarkdownToken.EMPHASIS_UNDERLINE;
}
}
case '-' -> {
if (isPrefix("--")) {
currentChar += 2;
return MarkdownToken.STRIKEOUT;
}
}
case '`' -> {
if (isPrefix("`")) {
currentChar += 1;
return MarkdownToken.CODE;
}
}
case '\\' -> {
if (isScreening(block.charAt(currentChar + 1))) {
currentChar++;
return MarkdownToken.SCREENING;
}
}
default -> {
return MarkdownToken.WORD;
}
}
return MarkdownToken.WORD;
}
private static boolean isScreening(char ch) {
return ch == '*' || ch == '_';
}
private boolean isSuffix(MarkdownToken token) {
if (token == MarkdownToken.WORD) {
return false;
}
String suffix = getHighlight(token);
if (isWordInBlock(suffix, currentChar, currentChar + suffix.length())) {
currentChar += suffix.length() - 1;
return true;
}
return false;
}
private boolean isPrefix(String prefix) {
int i = currentChar + prefix.length();
return (
isValidIndexInBlock(i + prefix.length()) &&
!isWordInBlock(prefix, i, i + prefix.length()) &&
isWordInBlock(prefix, i - prefix.length(), i) &&
!Character.isWhitespace(block.charAt(i))
);
}
private boolean isWordInBlock(String word, int start, int end) {
return word.equals(block.substring(start, end));
}
private boolean isValidIndexInBlock(int index) {
return index < block.length();
}
private boolean isHeader() {
int levelOfHeader = 0;
if (block.charAt(levelOfHeader) != '#') {
return false;
}
while (
levelOfHeader < block.length() && block.charAt(levelOfHeader) == '#'
) {
levelOfHeader++;
}
return (
levelOfHeader < block.length() &&
Character.isWhitespace(block.charAt(levelOfHeader))
);
}
private static String getHighlight(MarkdownToken token) {
switch (token) {
case STRONG_STAR -> {
return "**";
}
case STRONG_UNDERLINE -> {
return "__";
}
case EMPHASIS_STAR -> {
return "*";
}
case EMPHASIS_UNDERLINE -> {
return "_";
}
case CODE -> {
return "`";
}
case STRIKEOUT -> {
return "--";
}
case SCREENING -> {
return "\\";
}
default -> {
return "";
}
}
}
private static void addToList(
List<PartOfParagraph> items,
StringBuilder sb
) {
if (!sb.isEmpty()) {
items.add(new Text(sb.toString()));
sb.setLength(0);
}
}
}

View File

@@ -0,0 +1,8 @@
/**
* Tests for <a href="https://www.kgeorgiy.info/courses/prog-intro/homeworks.html#md2html">Markdown to HTML</a> homework
* of <a href="https://www.kgeorgiy.info/courses/prog-intro/">Introduction to Programming</a> course.
*
*
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
*/
package md2html;