md2x/source/converter.d

180 lines
3.8 KiB
D

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 ~= "<html><head></head><body>";
foreach(tree; md.children)
ap ~= convert(tree);
ap ~= "</body></html>";
return ap[];
}
case TreeType.line: {
auto ap = appender!string;
ap ~= "<p>";
foreach(tree; md.children)
ap ~= convert(tree);
ap ~= "</p>";
return ap[];
}
case TreeType.text:
return escape(md.text);
case TreeType.bold:
return "<b>"~escape(md.text)~"</b>";
case TreeType.italic:
return "<i>"~escape(md.text)~"</i>";
case TreeType.link:
return `<a href="`~escapeLink(md.link.link)~`">`~escape(md.link.label)~`</a>`;
case TreeType.header: {
auto ap = appender!string;
ap ~= "<h%u>".format(md.header.size);
foreach(tree; md.header.children)
ap ~= convert(tree);
ap ~= "</h%u>".format(md.header.size);
return ap[];
}
case TreeType.list: {
auto ap = appender!string;
ap ~= "<ul>";
foreach(tree; md.children)
ap ~= "<li>"~convert(tree)~"</li>";
ap ~= "</ul>";
return ap[];
}
}
}
@property string extension() { return "html"; }
static Converter instance;
static this() {
escapeTable = [
'&': "&amp;",
'<': "&lt;",
'>': "&gt;",
'"': "&quot;"
];
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; }
}