diff --git a/source/diet/html.d b/source/diet/html.d index b94cf62..3ba305e 100644 --- a/source/diet/html.d +++ b/source/diet/html.d @@ -393,6 +393,62 @@ unittest { test!"#foo"("
"); } +// test live mode works with HTML changes +unittest { + void test(string before, string after)(string expectedBefore, string expectedAfter) { + import std.array : appender, array; + import std.algorithm : splitter, equal, filter, startsWith; + import std.string : lineSplitter; + static const bef = parseDiet(before); + static const aft = parseDiet(after); + + enum _codeBefore = getHTMLLiveMixin(bef); + enum _codeAfter = getHTMLLiveMixin(aft); + + // ensure both items produce the same code + assert( _codeBefore.lineSplitter.filter!(l => !l.startsWith("#line")) + .equal(_codeAfter.lineSplitter.filter!(l => !l.startsWith("#line")))); + + + // test both sets of code with both strings + auto _diet_html_strings = getHTMLRawTextOnly(bef).splitter('\0').array; + { + auto _diet_output = appender!string(); + mixin(_codeBefore); + assert(_diet_output.data == expectedBefore, _diet_output.data); + } + { + auto _diet_output = appender!string(); + mixin(_codeAfter); + assert(_diet_output.data == expectedBefore, _diet_output.data); + } + + // second set of strings + _diet_html_strings = getHTMLRawTextOnly(aft).splitter('\0').array; + { + auto _diet_output = appender!string(); + mixin(_codeBefore); + assert(_diet_output.data == expectedAfter, _diet_output.data); + } + { + auto _diet_output = appender!string(); + mixin(_codeAfter); + assert(_diet_output.data == expectedAfter, _diet_output.data); + } + } + + // test renaming things + test!("foo(test=2+3)", + "foobar(testbaz=2+3)") + ("", + ""); + + // test injecting extra html + test!("- if(true)\n - auto x = 5;\n foo #{x}", + "- if(true)\n a(href=\"injected!\") injected html!\n - auto x = 5;\n foo #{x}", + )("5", "injected html!5"); +} + /** Determines how the generated HTML gets styled. @@ -507,22 +563,25 @@ private string getElementMixin(ref CTX ctx, in Node node, bool in_pre) @safe continue; } + // note the attribute name is HTML, and not code, so live mode + // should reprocess that and use the string table. ret ~= ctx.statement(node.loc, q{ static if (is(typeof(() { return %s; }()) == bool) ) }~'{', expr); + ret ~= ctx.statementCont(node.loc, q{if (%s)}, expr); if (ctx.isHTML5) - ret ~= ctx.statement(node.loc, q{if (%s) %s.put(" %s");}, expr, ctx.rangeName, att.name); + ret ~= ctx.rawText(node.loc, " "~att.name); else - ret ~= ctx.statement(node.loc, q{if (%s) %s.put(" %s=\"%s\"");}, expr, ctx.rangeName, att.name, att.name); + ret ~= ctx.rawText(node.loc, " "~att.name~"=\""~att.name~"\""); ret ~= ctx.statement(node.loc, "} else "~q{static if (is(typeof(%s) : const(char)[])) }~"{{", expr); - ret ~= ctx.statement(node.loc, q{ auto _diet_val = %s;}, expr); - ret ~= ctx.statement(node.loc, q{ if (_diet_val !is null) }~'{'); + ret ~= ctx.statementCont(node.loc, q{ auto _diet_val = %s;}, expr); + ret ~= ctx.statementCont(node.loc, q{ if (_diet_val !is null) }~'{'); ret ~= ctx.rawText(node.loc, " "~att.name~"=\""); ret ~= ctx.statement(node.loc, q{ %s.filterHTMLAttribEscape(_diet_val);}, ctx.rangeName); ret ~= ctx.rawText(node.loc, "\""); ret ~= ctx.statement(node.loc, " }"); - ret ~= ctx.statement(node.loc, "}} else {"); + ret ~= ctx.statementCont(node.loc, "}} else {"); } ret ~= ctx.rawText(node.loc, " "~att.name ~ "=\""); @@ -744,6 +803,22 @@ private struct CTX { return piecesMapOutputStr; } + // same as statement, but with guaranteed no raw text between the last + // statement and it. + pure string statementCont(ARGS...)(Location loc, string fmt, ARGS args) + { + import std.string : format; + with(OutputMode) final switch(mode) + { + case live: + case normal: + return ("#line %s \"%s\"\n"~fmt~"\n").format(loc.line+1, loc.file, args); + case rawTextOnly: + // do not output anything here, no raw text is possible + return ""; + } + } + pure string statement(ARGS...)(Location loc, string fmt, ARGS args) { import std.string : format, strip;