module converter; import std.array, std.algorithm, std.string, std.format; import parser; enum Format { none, html, gemtext, ast } interface Converter { string convert(Tree md); @property string extension(); static Converter format(Format f) { final switch(f) { case Format.html: return HTMLConverter.instance; case Format.gemtext: return GemtextConverter.instance; case Format.ast: return ASTConverter.instance; case Format.none: throw new Exception("Invalid format type."); } } } // escapes links private string escapeLink(string link) { static const string linkSpecialChars = ` !"#$%'()*+,-;<=>?@\`; auto ap = appender!string; foreach(i, c; link) { if(linkSpecialChars.canFind(c)) ap ~= "%%%02x".format(c); else ap ~= c; } return ap[]; } private class HTMLConverter : Converter { private { static string[dchar] escapeTable; // converts html special chars to html entities string escape(string s) { return s.translate(escapeTable); } } string convert(Tree md) { final switch(md.type) { case TreeType.top: { auto ap = appender!string; ap ~= ""; foreach(tree; md.children) ap ~= convert(tree); ap ~= ""; return ap[]; } case TreeType.line: { auto ap = appender!string; ap ~= "

"; foreach(tree; md.children) ap ~= convert(tree); ap ~= "

"; return ap[]; } case TreeType.text: return escape(md.text); case TreeType.bold: return ""~escape(md.text)~""; case TreeType.italic: return ""~escape(md.text)~""; case TreeType.link: return ``~escape(md.link.label)~``; case TreeType.header: { auto ap = appender!string; ap ~= "".format(md.header.size); foreach(tree; md.header.children) ap ~= convert(tree); ap ~= "".format(md.header.size); return ap[]; } case TreeType.list: { auto ap = appender!string; ap ~= ""; return ap[]; } } } @property string extension() { return "html"; } static Converter instance; static this() { escapeTable = [ '&': "&", '<': "<", '>': ">", '"': """ ]; instance = new HTMLConverter; } } private class GemtextConverter : Converter { string convert(Tree md) { final switch(md.type) { case TreeType.top: { auto ap = appender!string; foreach(tree; md.children) ap ~= convert(tree); ap ~= "\n"; return ap[]; } case TreeType.line: { auto ap = appender!string; foreach(tree; md.children) ap ~= convert(tree); ap ~= '\n'; return ap[]; } case TreeType.text: return md.text; case TreeType.italic: return "/"~md.text~"/"; case TreeType.bold: return "*"~md.text~"*"; case TreeType.link: return "\n=> "~escapeLink(md.link.link)~" "~md.link.label~"\n"; case TreeType.header: { auto ap = appender!string; auto h = md.header; if(h.size > 3) ap ~= '['; else { for(uint i = 0; i < h.size; i++) ap ~= '#'; ap ~= ' '; } foreach(tree; md.header.children) ap ~= convert(tree); if(h.size > 3) ap ~= ']'; ap ~= '\n'; return ap[]; } case TreeType.list: { auto ap = appender!string; foreach(tree; md.children) ap ~= "* "~convert(tree); ap ~= "\n"; return ap[]; } } } @property string extension() { return "gmi"; } static Converter instance; static this() { instance = new GemtextConverter; } } // not even really a converter class ASTConverter : Converter { string convert(Tree md) { return md.toString(); } @property string extension() { return "txt"; } static Converter instance; static this() { instance = new ASTConverter; } }