diff --git a/source/diet/html.d b/source/diet/html.d index e837bcf..a81c5f5 100644 --- a/source/diet/html.d +++ b/source/diet/html.d @@ -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. @@ -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)"(""); - test!"doctype html X\nfoo(test=true)"(""); - test!"doctype X\nfoo(test=true)"(""); + test!"doctype html\nfoo(test=true)"("", true); + test!"doctype html X\nfoo(test=true)"("", true); + test!"doctype X\nfoo(test=true)"("", true); test!"foo(test=2+3)"(""); test!"foo(test='#{2+3}')"(""); test!"foo #{2+3}"("5"); @@ -218,9 +277,9 @@ unittest { test!"- foreach (i; 0 .. 2)\n\tfoo"(""); test!"div(*ngFor=\"\\#item of list\")"( "
" - ); - test!".foo"("
"); - test!"#foo"("
"); + , true); + test!".foo"("
", true); + test!"#foo"("
", true); } @@ -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; @@ -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); @@ -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; } @@ -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--; @@ -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); } } @@ -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; @@ -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, ""); return ret; @@ -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; } @@ -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"; } diff --git a/source/diet/input.d b/source/diet/input.d index 923d5aa..0860bbc 100644 --- a/source/diet/input.d +++ b/source/diet/input.d @@ -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; @@ -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; diff --git a/source/diet/traits.d b/source/diet/traits.d index 04fde25..0edafea 100644 --- a/source/diet/traits.d +++ b/source/diet/traits.d @@ -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 { @@ -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 {