Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added runtime "static templates" fix #16 #56

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 98 additions & 26 deletions source/diet/html.d
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,44 @@ template compileHTMLDietStrings(alias FILES_GROUP, ALIASES...)
}
}


/** Renders a static diet file (without embedded D code) to HTML at runtime.

See_Also: `compileHTMLDietFile` */
string renderStaticHTMLDietFile(ALIASES...)(string file)
{
import std.file : readText;

return renderStaticHTMLDietStrings!ALIASES(collectFilesRT(file, readText(file)));
}

/** Renders static diet code (without embedded D code) to HTML at runtime.

See_Also: `compileHTMLDietString` */
string renderStaticHTMLDietString(ALIASES...)(string code, string filename = "string")
{
InputFile[1] f;
f[0].name = filename;
f[0].contents = code;
return renderStaticHTMLDietStrings!ALIASES(f[]);
}

/** Renders a static diet file hierarchy (without embedded D code) to HTML at runtime.

See_Also: `compileHTMLDietStrings` */
string renderStaticHTMLDietStrings(ALIASES...)(InputFile[] files)
{
alias TRAITS = DietTraits!ALIASES;
return renderHTML(applyTraits!TRAITS(parseDiet!(translate!TRAITS)(files)));
}

/** Returns a mixin string that generates HTML 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`.
style = compact or pretty HTML output style.

Returns:
A string of D statements suitable to be mixed in inside of a function.
Expand All @@ -191,25 +223,52 @@ string getHTMLMixin(in Document doc, string range_name = dietOutputRangeName, HT
string ret = "import diet.internal.html : htmlEscape, htmlAttribEscape;\n";
ret ~= "import std.format : formattedWrite;\n";
foreach (i, n; doc.nodes)
ret ~= ctx.getHTMLMixin(n, false);
ret ~= ctx.getHTMLMixin(n, false, false);
ret ~= ctx.flushRawText();
return ret;
}

/** Returns the rendered HTML for a static diet file without embedded D code.

Params:
style = compact or pretty HTML output style.

Returns:
A raw string of the HTML
*/
string renderHTML(in Document doc, HTMLOutputStyle style = HTMLOutputStyle.compact)
{
CTX ctx;
ctx.pretty = style == HTMLOutputStyle.pretty;
ctx.runtime = true;
string ret;
foreach (n; doc.nodes)
ret ~= ctx.getHTMLMixin(n, false, true);
return ret;
}

unittest {
import diet.parser;
void test(string src)(string expected) {
void test(string src)(string expected, bool runtime = false) {
import std.array : appender;

// test compile time
static const n = parseDiet(src);
auto _diet_output = appender!string();
//pragma(msg, getHTMLMixin(n));
mixin(getHTMLMixin(n));
assert(_diet_output.data == expected, _diet_output.data);

if (runtime) {
// test runtime
string rt = renderHTML(n);
assert(rt == expected, rt);
}
}

test!"doctype html\nfoo(test=true)"("<!DOCTYPE html><foo test></foo>");
test!"doctype html X\nfoo(test=true)"("<!DOCTYPE html X><foo test=\"test\"></foo>");
test!"doctype X\nfoo(test=true)"("<!DOCTYPE X><foo test=\"test\"/>");
test!"doctype html\nfoo(test=true)"("<!DOCTYPE html><foo test></foo>", true);
test!"doctype html X\nfoo(test=true)"("<!DOCTYPE html X><foo test=\"test\"></foo>", true);
test!"doctype X\nfoo(test=true)"("<!DOCTYPE X><foo test=\"test\"/>", true);
test!"foo(test=2+3)"("<foo test=\"5\"></foo>");
test!"foo(test='#{2+3}')"("<foo test=\"5\"></foo>");
test!"foo #{2+3}"("<foo>5</foo>");
Expand All @@ -218,9 +277,9 @@ unittest {
test!"- foreach (i; 0 .. 2)\n\tfoo"("<foo></foo><foo></foo>");
test!"div(*ngFor=\"\\#item of list\")"(
"<div *ngFor=\"#item of list\"></div>"
);
test!".foo"("<div class=\"foo\"></div>");
test!"#foo"("<div id=\"foo\"></div>");
, true);
test!".foo"("<div class=\"foo\"></div>", true);
test!"#foo"("<div id=\"foo\"></div>", true);
}


Expand Down Expand Up @@ -259,27 +318,27 @@ private @property template getHTMLOutputStyle(TRAITS...)
} else enum getHTMLOutputStyle = HTMLOutputStyle.compact;
}

private string getHTMLMixin(ref CTX ctx, in Node node, bool in_pre)
private string getHTMLMixin(ref CTX ctx, in Node node, bool in_pre, bool runtime)
{
switch (node.name) {
default: return ctx.getElementMixin(node, in_pre);
default: return ctx.getElementMixin(node, in_pre, runtime);
case "doctype": return ctx.getDoctypeMixin(node);
case Node.SpecialName.code: return ctx.getCodeMixin(node, in_pre);
case Node.SpecialName.comment: return ctx.getCommentMixin(node);
case Node.SpecialName.code: return ctx.getCodeMixin(node, in_pre, runtime);
case Node.SpecialName.comment: return ctx.getCommentMixin(node, runtime);
case Node.SpecialName.hidden: return null;
case Node.SpecialName.text:
string ret;
foreach (i, c; node.contents)
ret ~= ctx.getNodeContentsMixin(c, in_pre);
ret ~= ctx.getNodeContentsMixin(c, in_pre, runtime);
if (in_pre) ctx.plainNewLine();
else ctx.prettyNewLine();
return ret;
}
}

private string getElementMixin(ref CTX ctx, in Node node, bool in_pre)
private string getElementMixin(ref CTX ctx, in Node node, bool in_pre, bool runtime)
{
import std.algorithm : countUntil;
import std.algorithm : all, countUntil;

if (node.name == "pre") in_pre = true;

Expand Down Expand Up @@ -337,6 +396,11 @@ private string getElementMixin(ref CTX ctx, in Node node, bool in_pre)
continue;
}

if (expr.all!(a => a >= '0' && a <= '9')) {
ret ~= ctx.rawText(node.loc, " "~att.name~"=\""~expr~"\"");
continue;
}

ret ~= ctx.statement(node.loc, q{
static if (is(typeof(() { return %s; }()) == bool) )
}~'{', expr);
Expand All @@ -363,6 +427,7 @@ private string getElementMixin(ref CTX ctx, in Node node, bool in_pre)
ret ~= ctx.rawText(node.loc, htmlAttribEscape(v.value));
break;
case interpolation, rawInterpolation:
enforcep(!runtime, "Code interpolation not allowed in static runtime diet templates.", node.loc);
ret ~= ctx.statement(node.loc, q{%s.htmlAttribEscape(%s);}, ctx.rangeName, v.value);
break;
}
Expand Down Expand Up @@ -392,7 +457,7 @@ private string getElementMixin(ref CTX ctx, in Node node, bool in_pre)
}

foreach (i, c; node.contents)
ret ~= ctx.getNodeContentsMixin(c, in_pre);
ret ~= ctx.getNodeContentsMixin(c, in_pre, runtime);

if (need_newline && !in_pre) {
ctx.depth--;
Expand All @@ -411,16 +476,18 @@ private string getElementMixin(ref CTX ctx, in Node node, bool in_pre)
return ret;
}

private string getNodeContentsMixin(ref CTX ctx, in NodeContent c, bool in_pre)
private string getNodeContentsMixin(ref CTX ctx, in NodeContent c, bool in_pre, bool runtime)
{
final switch (c.kind) with (NodeContent.Kind) {
case node:
return getHTMLMixin(ctx, c.node, in_pre);
return getHTMLMixin(ctx, c.node, in_pre, runtime);
case text:
return ctx.rawText(c.loc, c.value);
case interpolation:
enforcep(!runtime, "Code interpolation not allowed in static runtime diet templates.", c.loc);
return ctx.textStatement(c.loc, q{%s.htmlEscape(%s);}, ctx.rangeName, c.value);
case rawInterpolation:
enforcep(!runtime, "Code interpolation not allowed in static runtime diet templates.", c.loc);
return ctx.textStatement(c.loc, q{() @trusted { return (&%s); } ().formattedWrite("%%s", %s);}, ctx.rangeName, c.value);
}
}
Expand Down Expand Up @@ -484,8 +551,9 @@ private string getDoctypeMixin(ref CTX ctx, in Node node)
return ctx.rawText(node.loc, "<"~doctype_str~">");
}

private string getCodeMixin(ref CTX ctx, in ref Node node, bool in_pre)
private string getCodeMixin(ref CTX ctx, in ref Node node, bool in_pre, bool runtime)
{
enforcep(!runtime, "Code blocks not allowed in static runtime diet templates. ", node.loc);
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;
Expand All @@ -498,19 +566,19 @@ private string getCodeMixin(ref CTX ctx, in ref Node node, bool in_pre)
got_code = true;
} else {
assert(c.kind == NodeContent.Kind.node);
ret ~= ctx.getHTMLMixin(c.node, in_pre);
ret ~= ctx.getHTMLMixin(c.node, in_pre, runtime);
}
}
ret ~= ctx.statement(node.loc, "}");
return ret;
}

private string getCommentMixin(ref CTX ctx, in ref Node node)
private string getCommentMixin(ref CTX ctx, in ref Node node, bool runtime)
{
string ret = ctx.rawText(node.loc, "<!--");
ctx.depth++;
foreach (i, c; node.contents)
ret ~= ctx.getNodeContentsMixin(c, false);
ret ~= ctx.getNodeContentsMixin(c, false, runtime);
ctx.depth--;
ret ~= ctx.rawText(node.loc, "-->");
return ret;
Expand All @@ -531,12 +599,16 @@ private struct CTX {
bool inRawText = false;
NewlineState newlineState = NewlineState.none;
bool anyText;
bool runtime;

pure string statement(ARGS...)(Location loc, string fmt, ARGS args)
{
import std.string : format;
string ret = flushRawText();
ret ~= ("#line %s \"%s\"\n"~fmt~"\n").format(loc.line+1, loc.file, args);
if (runtime)
ret ~= fmt.format(args);
else
ret ~= ("#line %s \"%s\"\n"~fmt~"\n").format(loc.line+1, loc.file, args);
return ret;
}

Expand All @@ -551,19 +623,19 @@ private struct CTX {
pure string rawText(ARGS...)(Location loc, string text)
{
string ret;
if (!this.inRawText) {
if (!this.inRawText && !runtime) {
ret = this.rangeName ~ ".put(\"";
this.inRawText = true;
}
ret ~= outputPendingNewline();
ret ~= dstringEscape(text);
ret ~= runtime ? text : dstringEscape(text);
anyText = true;
return ret;
}

pure string flushRawText()
{
if (this.inRawText) {
if (this.inRawText && !runtime) {
this.inRawText = false;
return "\");\n";
}
Expand Down
35 changes: 34 additions & 1 deletion source/diet/input.d
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,38 @@ template collectFiles(string root_file, alias root_contents)
else enum collectFiles = InputFile(root_file, root_contents) ~ baseFiles;
}

/// ditto
InputFile[] collectFilesRT(string file)
{
import std.file : readText;
return collectFilesRT(file, readText(file));
}
/// ditto
InputFile[] collectFilesRT(string file, string content)
{
import std.file : exists, readText;
import std.path : extension, buildPath, dirName;

string root = dirName(file);
InputFile[] ret = [InputFile(file, content)];
foreach (ofile; collectReferences(content))
{
string p = buildPath(root, ofile);
if (!exists(p))
p = buildPath(root, ofile ~ file.extension);
//if (!exists(p))
// p = ofile;
//if (!exists(p))
// p = ofile ~ file.extension;
if (!exists(p))
continue;
string ocontent = readText(p);
ret ~= InputFile(ofile, ocontent);
ret ~= collectFilesRT(ofile, ocontent);
}
return ret;
}

/// Encapsulates a single input file.
struct InputFile {
string name;
Expand Down Expand Up @@ -96,7 +128,8 @@ private template collectReferencedFiles(string file_name, alias file_contents)
alias collectReferencedFiles = impl!0;
}

private string[] collectReferences(string content)
/// Searches for `extends` and `include` nodes and returns all referenced strings.
string[] collectReferences(string content)
{
import std.string : strip, stripLeft, splitLines;
import std.algorithm.searching : startsWith;
Expand Down
40 changes: 17 additions & 23 deletions source/diet/traits.d
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ private string generateFilterChainMixin(string[] chain, NodeContent[] contents)

unittest {
import std.array : appender;
import diet.html : compileHTMLDietString;
import diet.html : compileHTMLDietString, renderStaticHTMLDietString;

@dietTraits
static struct CTX {
Expand All @@ -231,29 +231,23 @@ unittest {
CTX.filters["foo"] = (input, scope output) { output("(R"); output(input); output("R)"); };
CTX.filters["bar"] = (input, scope output) { output("(RB"); output(input); output("RB)"); };

auto dst = appender!string;
dst.compileHTMLDietString!(":foo text", CTX);
assert(dst.data == "(text)");

dst = appender!string;
dst.compileHTMLDietString!(":foo text\n\tmore", CTX);
assert(dst.data == "(text\nmore)");

dst = appender!string;
dst.compileHTMLDietString!(":foo :foo text", CTX);
assert(dst.data == "((text))");

dst = appender!string;
dst.compileHTMLDietString!(":bar :foo text", CTX);
assert(dst.data == "(RB(text)RB)");

dst = appender!string;
dst.compileHTMLDietString!(":foo :bar text", CTX);
assert(dst.data == "(R(RBtextRB)R)");
void test(string code)(string expected, bool runtime = true) {
auto dst = appender!string;
dst.compileHTMLDietString!(code, CTX);
assert(dst.data == expected);
if (runtime) {
string rt = renderStaticHTMLDietString!CTX(code);
assert(rt == expected);
}
}

dst = appender!string;
dst.compileHTMLDietString!(":foo text !{1}", CTX);
assert(dst.data == "(Rtext 1R)");
test!(":foo text")("(text)");
test!(":foo text\n\tmore")("(text\nmore)");
test!(":foo :foo text")("((text))");
// TODO: the next 2 should work with runtime too!
test!(":bar :foo text")("(RB(text)RB)", false);
test!(":foo :bar text")("(R(RBtextRB)R)", false);
test!(":foo text !{1}")("(Rtext 1R)", false);
}

@safe unittest {
Expand Down