Skip to content

Instantly share code, notes, and snippets.

@WebFreak001
Created August 18, 2019 13:15
Show Gist options
  • Save WebFreak001/b07136abe6d977de3ccda76f8d1e606b to your computer and use it in GitHub Desktop.
Save WebFreak001/b07136abe6d977de3ccda76f8d1e606b to your computer and use it in GitHub Desktop.
module diet2spasm;
import diet.defs;
import diet.dom;
import diet.internal.string;
import diet.input;
import diet.parser;
import diet.traits;
/// Compiles diet code to spasm compatible createElement/textContent/setAttribute D code usable for mixin.
/// Elements with an ID set are accessible via their ID in the resulting code.
/// You have to manually append all root nodes into some container if you pass container=null.
template compileSpasmDietFile(string filename, string container = null, TRAITS...)
{
import diet.internal.string : stripUTF8BOM;
private static immutable contents = stripUTF8BOM(import(filename));
enum compileSpasmDietFile = compileSpasmDietFileString!(filename, contents, container, TRAITS);
}
/// ditto
template compileSpasmDietFileString(string filename, alias contents,
string container = null, TRAITS...)
{
enum _diet_files = collectFiles!(filename, contents);
pragma(msg, "Compiling Diet HTML template " ~ filename ~ "...");
private Document _diet_nodes()
{
return applyTraits!TRAITS(parseDiet!(translate!TRAITS)(_diet_files));
}
enum compileSpasmDietFileString = getSpasmMixin(_diet_nodes(), container);
}
/// ditto
template compileSpasmDietString(string contents, string container = null, TRAITS...)
{
enum compileSpasmDietString = compileSpasmDietStrings!(Group!(contents,
"diet-string"), container, TRAITS);
}
/// ditto
template compileSpasmDietStrings(alias FILES_GROUP, string container = null, TRAITS...)
{
private static Document _diet_nodes()
{
return applyTraits!TRAITS(parseDiet!(translate!TRAITS)(filesFromGroup!FILES_GROUP));
}
enum compileSpasmDietStrings = getSpasmMixin(_diet_nodes(), container);
}
@safe:
/** Returns a mixin string that generates spasm code for the given DOM tree.
Params:
nodes = The root nodes of the DOM tree
range_name = Optional custom name to use for the output range, defaults
to `_diet_output`.
Returns:
A string of D statements suitable to be mixed in inside of a function.
*/
string getSpasmMixin(in Document doc, string parent = null)
{
import std.conv : text;
string ret;
foreach (i, n; doc.nodes)
ret ~= getSpasmMixin(doc.nodes[0], text("_node", i), parent);
return ret;
}
private string getSpasmMixin(in Node node, string id, string parent)
{
import std.conv : text;
switch (node.name)
{
default:
return getElementMixin(node, id, parent);
case Node.SpecialName.text:
string ret;
foreach (i, c; node.contents)
ret ~= getNodeContentsMixin(c, text(id, "_c", i), parent);
return ret;
case Node.SpecialName.code:
return getCodeMixin(node, id, parent);
case "doctype":
case Node.SpecialName.comment:
case Node.SpecialName.hidden:
return null;
}
}
private string getNodeContentsMixin(in NodeContent c, string id, string parent)
{
final switch (c.kind) with (NodeContent.Kind)
{
case node:
return getSpasmMixin(c.node, id, parent);
case text:
return makeStatement(c.loc, `%s.appendChild(document.createTextNode("%s"));`,
parent, c.value.dstringEscape);
case interpolation:
return makeStatement(c.loc, `%s.appendChild(document.createTextNode(%s));`, parent, c.value);
case rawInterpolation:
return makeStatement(c.loc, `%s.insertAdjacentHTML("beforeend", %s);`, parent, c.value);
}
}
private string getElementMixin(in Node node, string id, string parent)
{
import std.conv : text;
if (node.id.contents.length == 1 && node.id.contents[0].kind == AttributeContent.Kind.text)
id = node.id.contents[0].value;
string tagname = node.name.length ? node.name : "div";
string ret;
ret ~= makeStatement(node.loc, "auto %s = document.createElement(`%s`);", id, tagname);
bool had_class = false;
// write attributes
foreach (ai, att_; node.attributes)
{
auto att = att_.dup; // this sucks...
// merge multiple class attributes into one
if (att.name == "class")
{
if (had_class)
continue;
had_class = true;
foreach (ca; node.attributes[ai + 1 .. $])
{
if (ca.name != "class")
continue;
if (!ca.contents.length || (()@trusted => (ca.isText && !ca.expectText.length))())
continue;
att.addText(" ");
att.addContents(ca.contents);
}
}
const is_expr = att.contents.length == 1
&& att.contents[0].kind == AttributeContent.Kind.interpolation;
if (is_expr)
{
auto expr = att.contents[0].value;
if (expr == "true")
{
ret ~= makeStatement(node.loc, "%s.setAttribute(`%s`, `%s`);", id, att.name, att.name);
continue;
}
ret ~= makeStatement(node.loc, q{
static if (is(typeof(() { return %s; }()) == bool) )
} ~ '{', expr);
ret ~= makeStatement(node.loc, q{if (%s) %s.setAttribute(`%s`, `%s`); },
expr, id, att.name, att.name);
ret ~= makeStatement(node.loc,
"} else " ~ q{static if (is(typeof(%s) : const(char)[])) } ~ "{{", expr);
ret ~= makeStatement(node.loc, q{ auto _diet_val = %s;}, expr);
ret ~= makeStatement(node.loc, q{ if (_diet_val !is null) } ~ '{');
ret ~= makeStatement(node.loc, q{ %s.setAttribute(`%s`, _diet_val);}, id, att.name);
ret ~= makeStatement(node.loc, " }");
ret ~= makeStatement(node.loc, "}} else {");
}
ret ~= makeStatement(node.loc, "%s.setAttribute(`%s`, text(", id, att.name);
foreach (i, v; att.contents)
{
final switch (v.kind) with (AttributeContent.Kind)
{
case text:
ret ~= makeStatement(node.loc, `"%s", `, dstringEscape(v.value));
break;
case interpolation, rawInterpolation:
ret ~= makeStatement(node.loc, `%s, `, v.value);
break;
}
}
ret ~= "));\n";
if (is_expr)
ret ~= makeStatement(node.loc, "}");
}
foreach (i, c; node.contents)
ret ~= getNodeContentsMixin(c, text(id, "_c", i), id);
if (parent.length)
ret ~= makeStatement(node.loc, "%s.appendChild(%s);", parent, id);
return ret;
}
private string getCodeMixin(in ref Node node, string id, string parent)
{
(() @trusted {
enforcep(node.attributes.length == 0, "Code lines may not have attributes.", node.loc);
enforcep(node.attribs == NodeAttribs.none,
"Code lines may not specify translation or text block suffixes.", node.loc);
})();
if (node.contents.length == 0)
return null;
string ret;
foreach (i, c; node.contents)
{
if (i == 0 && c.kind == NodeContent.Kind.text)
{
ret ~= makeStatement(node.loc, "%s {", c.value);
}
else
{
assert(c.kind == NodeContent.Kind.node);
ret ~= getSpasmMixin(c.node, id, parent);
}
}
ret ~= makeStatement(node.loc, "}");
return ret;
}
private string makeStatement(Args...)(Location loc, string fmt, Args args)
{
import std.string : format;
return ("#line %s \"%s\"\n" ~ fmt ~ "\n").format(loc.line + 1, loc.file, args);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment