Skip to content

Instantly share code, notes, and snippets.

@DarkSeraphim
Created June 10, 2019 13:58
Show Gist options
  • Save DarkSeraphim/30d7f6cd1bdd6efb0f38163ed99215bc to your computer and use it in GitHub Desktop.
Save DarkSeraphim/30d7f6cd1bdd6efb0f38163ed99215bc to your computer and use it in GitHub Desktop.
Formatting Markdown to HTML
import java.util.Deque;
import java.util.Objects;
import java.util.Queue;
import java.util.Set;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.HashMap;
class MarkdownFormatter {
static class FormatToken {
private final String tag;
private StringBuilder content = null;
FormatToken(String tag) {
this.tag = tag;
}
public String getTag() {
return this.tag;
}
public int hashCode() {
return this.tag.hashCode();
}
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o == null || o.getClass() != this.getClass()) {
return false;
}
FormatToken that = (FormatToken) o;
return this.tag.equals(that.tag);
}
public void appendContent(StringBuilder content) {
if (this.content == null) {
this.content = content;
} else {
this.content.append(content);
}
}
public StringBuilder getContent() {
return this.content;
}
}
private static class HtmlTag {
private final String open, close;
public HtmlTag(String name) {
this(String.format("<%s>", name), String.format("</%s>", name));
}
public HtmlTag(String open, String close) {
this.open = open;
this.close = close;
}
public String getOpenTag() {
return this.open;
}
public String getCloseTag() {
return this.close;
}
}
private final Set<Character> special = new HashSet<>();
private final Map<FormatToken, HtmlTag> html = new HashMap<>();
public MarkdownFormatter() {
this.registerTag("**", new HtmlTag("strong"));
this.registerTag("_", new HtmlTag("em"));
this.registerTag("~", new HtmlTag("s"));
this.registerTag("`", new HtmlTag("code"));
}
private void registerTag(String tag, HtmlTag html) {
this.special.add(tag.charAt(0));
this.html.put(new FormatToken(tag), html);
}
public String toHTML(String input) {
Deque<FormatToken> formattingStack = new LinkedList<>();
Map<FormatToken, Integer> present = new HashMap<>();
FormatToken head = null;
StringBuilder result = new StringBuilder();
int lm1 = input.length() - 1;
for (int i = 0; i < input.length(); i++) {
char c = input.charAt(i);
if (special.contains(c)) {
StringBuilder sb = new StringBuilder(3).append(c);
while (i < lm1 && input.charAt(i + 1) == c) {
sb.append(c);
i++;
}
FormatToken token = new FormatToken(sb.toString());
if (present.containsKey(token)) {
sb = new StringBuilder();
FormatToken open;
while ((open = formattingStack.pollLast()) != null) {
if (open.getContent() != null) {
sb.insert(0, open.getContent());
}
present.computeIfPresent(open, ($, counter) -> counter != 1 ? counter - 1 : null);
if (open.equals(token)) {
// Matching but unknown tags should be treated as is
HtmlTag tag = this.html.computeIfAbsent(open, o -> new HtmlTag(o.getTag(), token.getTag()));
sb.insert(0, tag.getOpenTag());
sb.append(tag.getCloseTag());
break;
} else {
sb.insert(0, open.getTag());
}
}
head = formattingStack.peekLast();
if (head == null) {
result.append(sb);
} else {
head.appendContent(sb);
}
} else {
present.merge(token, 1, Integer::sum);
head = token;
formattingStack.offer(token);
}
} else {
StringBuilder sb = new StringBuilder().append(c);
while (i < lm1 && !special.contains(input.charAt(i + 1))) {
sb.append(input.charAt(i + 1));
i++;
}
if (head == null) {
result.append(sb);
} else {
head.appendContent(sb);
}
}
}
return result.toString();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment