diet-ng-1.8.1/0000755000175000017500000000000014230473643012404 5ustar nileshnileshdiet-ng-1.8.1/SPEC.md0000644000175000017500000003210614230473643013462 0ustar nileshnileshDiet Language Specification =========================== (NOTE: This specification is not yet complete. To fill the gaps, you can orient yourself using the [documentation on vibed.org](https://vibed.org/templates/diet) and the [pugjs reference](https://pugjs.org/api/reference.html)). Synopsis -------- This is an example of a simple Diet HTML template: doctype html - auto title = "Hello, "; html head title #{title} - example page body h1= title h2 Index ol.pageindex - foreach (i; 0 .. 3) li: a(href="##{i}") Point #{i} - foreach (i; 0 .. 3) h2(id=i) Point #{i} p. These are the #[i contents] of point #{i}. Multiple lines of text are contained in this paragraph. The generated HTML code will look like this: Hello, <World> - example page

Hello, <World>

Index

  1. Point 0
  2. Point 1
  3. Point 2

Point 0

These are the contents of point 0. Multiple lines of text are contained in this paragraph.

Point 1

These are the contents of point 1. Multiple lines of text are contained in this paragraph.

Point 2

These are the contents of point 2. Multiple lines of text are contained in this paragraph.

Indentation ----------- Diet templates are a hierarchical data format, where the nesting of the data is determined using the line indentation level (similar to Python). The indentation style is determined from the first line in the file that is indented. The sequence of space and tab characters prefixed to the line's contents will be taken as a template for all following lines. The white space in front of all lines in the document must be a multiple of this whitespace sequence (concatenation). The only exceptions are the nested contents of comments and text nodes. Tags ---- A tag consists of the following parts, which, if present, must occur in the listed order: - **Tag name** The name must be an *identifier*, that may additionally contain the following characters: `-_:`. Without any additional parts this will output an element `` or if it can have children (everything except a few selected [HTML tags](source/diet/html.d#291)) it will surround the children with `` instead. The tag name can be omitted if an element ID or a class name is present, in which case it will default to `div` for HTML templates. - **Element ID** The ID must be prefixed by a `#` character and must be a valid *identifier* with the following additionally permitted characters: `-_`. For HTML templates, the ID will be output as an "id" attribute of the HTML element. There must only be one ID per element. If the tag name has been omitted but an ID is present, the tag name will be `div` for HTML templates. - **Style class list** List of class names, where each class name is prefixed by a `.`. A class name must be a valid *identifier* with the following additionally permitted characters: `-_`. For HTML templates, multiple class names will be merged into a single "class" attribute (classes separated by space characters). If the tag name has been omitted but at least one class name is present, the tag name will be `div` for HTML templates. - **Attribute list** List of attributes of the form `(att1=value, att2)`. Attributes can have new lines but the children must still be indented as usual. An attribute name must be a valid *identifier*. The value part can take any of the following forms: - a valid D expression ```d a(href=user.url) Me // Generates (user = {url: "/bob"}) Me ``` which gets compiled into the executable and can use runtime values passed via the render function, see [Embedded D code](#embedded-d-code). - a string literal with double-quotes `"` or single-quotes `'`, which may contain *interpolations* ```d img(src="/images/avatar_#{picture.id}.png") // Generates (picture = {id: 4}) ``` - a boolean value or no value part. It looks like a normal HTML5 shortened attribute but will generate valid XHTML attributes. ```d button(enabled) button(enabled=false) // Generates ``` - **Whitespace-removal directives** - A single `<` will instruct the generator not to emit additional white space within the generated HTML element. ```html div foo> a bar // Generates
bar ``` - A single `>` will instruct the generator not to emit additional white space around the generated HTML element. ```html div foo< a bar // Generates
test ``` You might also combine both whitespace-removal directives using `<>` or `><` which will get rid of all whitespaces associated with the tag inside the generated HTML. You can use this for example for a horizontal row of elements or buttons that shouldn't have any spaces in between them. - **Translation directive** A single `&` will mark the node's contents to be subject to translation (in i18n contexts) ``` h1& website.title ``` To implement the translate function you need to add a `static string translate(string text)` which must work at compile time inside your diet context. ```d @dietTraits struct Context { static string translate(string text) { return text == "Hello, World!" ? "Hallo, Welt!" : text; } } auto dst = appender!string; dst.compileHTMLDietFile!("diet.dt", Context); ``` or when using inside vibe.d you use it with a `translationContext`. Instead of a tag you may also place a `| text` node which will insert the raw text (`text` in this case) into the HTML document. You can use this to set a tag content to a combination of tags and text, or you could use foreach loops adding text with this, etc. Adding a second space will start inserting actual spaces into the inserted text as only everything after the `| ` is consumed, see [Text nodes](text-nodes). All parts are optional, except that at least one of tag name, id, or class name must be present. The text that follows the tag definition determines how the following text is interpreted when determining the node's contents: - Directly followed by `:`: Another tag can be put on the same line and will be nested: `li: a(href="https://...") link` - Directly followed by `!=`, the rest of the line is treated as a D expression that will be converted to a string and is then output verbatim in the result - Directly followed by `=`, the rest of the line is treated as a D expression that will be converted to a string and is then output in escaped form (HTML escaped for the HTML generator) - Directly followed by a dot (`.`), the following nested lines will all be treated as text, without using explicit *text nodes*. - Followed by a space character, the rest of the line is treated as text contents with the possibility to insert *interpolations* and *inline tags* ### Identifiers TODO! ### Inline tags Within text contents it is possible to insert nested nodes within the same line by enclosing them in `#[...]`. The syntax is the same as for normal tags, except that the `:` and `.` suffixes are not permitted. Example: `p This is #[em emphasized] text.` ### Inline HTML ### TODO! Text nodes ---------- Pure text content can be specified using the `|` prefix. The text that follows will be treated as contents of the parent node. The `&`, `=` and `!=` suffixes are supported and behave the same as for *tags*. Example: p This is a long | paragraph that is | split across multiple | lines. Comments -------- Comments are prefixed with `//`. The line itself, as well as any nested lines following it will be treated as contents of the comment. Adding a single dash (`//-`) will force the comment contents to not appear in the generated result. Otherwise, if the output format supports it, the contents will appear as a comment in the output. // Looking for a HTML job? jobs.localhost //- Password = 123456 Generates Embedded D code --------------- ### Statements D statements can be inserted by prefixing them with a single dash (`-`). A scope will be created around any nested contents, so that it is possible to use control statements. Example: - int i = 1; - i++; - if (i > 1) p OK - else p No! Will output `

OK

` Function declarations are also supported using their natural D syntax: - void foo(int i) - p= i - foo(0); - foo(1); Will output `

0

1

` ### Text interpolations D expressions can be embedded within text contents using the *text interpolation* syntax. The expression will first be converted to a string using the conversion rules of [std.conv](http://dlang.org/phobos/std_conv.html), and is then either properly escaped for the output format, or inserted verbatim. The syntax for escaped processing is `#{...}` and should always be used, unless the expression is expected to yield a string that has the same format as the output (e.g. HTML). For verbatim output, use `!{...}`. Any `#` or `!` that is not followed by a `{` (or `[` in case of inline tags) will be interpreted as a simple character. If these characters are supposed to be interpreted as characters despite being followed by a brace, the backslash character can be used to escape them. Example: p This text #{"cont"~"ains"} dynamically generated !{""~"text"~""}. p It uses the syntax \#{...} or \!{...}. Outputs:

This text contains dynamically generated text

It uses the syntax #{...} or !{...}.

### Interpolations in attributes These work almost the same as normal text interpolations, except that they obey different escaping rules that depend on the output format. Example: - foreach (i; 0 .. 3) p(class='text#{i % 2 ? "even" : "odd"}') #{i+1} Outputs:

1

2

3

Filters ------- `:filter1 :filter2 text` **TODO!** Includes -------- With includes it is possible to embed one template in another. The included template gets pasted at the position of the include-keyword. The indentation of the include-portion will propagate to the included template. Command: `include file(.ext)` Example: // main.dt doctype html html head title includeExample body h2 the following content is not in this file ... include otherfile // otherfile.dt h3 ... But In the other file and this include yetanotherfile // yetanotherfile.dt h4 in yet anotherfile Outputs: includeExample

the following content is not in this file ...

... But In the other file and this

in yet another file

In the case of error `Missing include input file` check [templates placement](#templates-placement). Blocks and Extensions --------------------- `extends file(.ext)` **TODO!** Extension Includes ------------------ Included templates can optionally be used as an extension base with its blocks being defined through the child nodes of the `include` tag. The extension logic is the same as for blocks and extensions, except that the control flow is reversed and base templates can serve as reusable components. Example: // section.dt h1 block title block contents // main.dt doctype html head title Include extensions body include section.dt block title | First section block contents p These are the contents of the first section. include section.dt block title | Second section block contents p These are the contents of the second section. Outputs: Include extensions

First section

These are the contents of the first section.

Second section

These are the contents of the second section.

HTML-specific Features ---------------------- ### Doctype Specifications `doctype ...` Legacy syntax: `!!! ...` will be transformed to `doctype ...` **TODO!** Templates placement ------------------- Diet looks for templates according to the list of directories specified in the parameter stringImportPaths of dub config file (see dub documentation for [json](https://code.dlang.org/package-format?lang=json#build-settings) or [sdl](https://code.dlang.org/package-format?lang=sdl#build-settings) format). Default value is `views/`. This applies to a method call `compileHTMLDietFile` and directives in the file being processed. `compileHTMLDietString` at the moment can not find include files by yourself, it is necessary to take additional steps (see answer [here](http://forum.rejectedsoftware.com/groups/rejectedsoftware.vibed/post/41058)). Grammar ------- **TODO!** diet-ng-1.8.1/.gitignore0000644000175000017500000000042614230473643014376 0ustar nileshnilesh*.[oa] *.so *.lib *.dll .*.sw* docs.json /docs/* .dub dub.selections.json # Mono-D files *.userprefs # Unittest binaries __test__*__ diet-ng-test-library # Examples examples/htmlgenerator/htmlgenerator examples/htmlgenerator/index.html examples/htmlserver/htmlserver *.exe diet-ng-1.8.1/CONTRIBUTING.md0000644000175000017500000000147114230473643014640 0ustar nileshnileshGuidelines for Contributing =========================== This repository follows the [vibe.d](https://vibed.org/) contribution guidelines. The following points should ideally apply to app pull requests: - Each pull request should contain only one isolated functional change - The code adheres to the [style guide](http://vibed.org/style-guide) - For the occasional more complex pull request each change should be separated into its own commit - Try not to mix whitespace or style changes with functional changes in the same commit - The pull request must pass the test suite (run `dub test` to test locally) Exceptions to these rules are accepted all the time, but please try to follow them as closely as possible, because otherwise it often considerably increases the total amount of work and communication overhead. diet-ng-1.8.1/.editorconfig0000644000175000017500000000024414230473643015061 0ustar nileshnileshroot = true [*.{c,h,d,di,dd,json}] end_of_line = lf insert_final_newline = true indent_style = tab indent_size = 4 trim_trailing_whitespace = true charset = utf-8 diet-ng-1.8.1/meson.build0000644000175000017500000000251514230473643014551 0ustar nileshnileshproject('Diet-NG', 'd', meson_version: '>=0.40', license: 'MIT', version: '1.7.4' ) project_soversion = '0' project_version = meson.project_version() pkgc = import('pkgconfig') # # Sources # diet_src = [ 'source/diet/defs.d', 'source/diet/dom.d', 'source/diet/html.d', 'source/diet/input.d', 'source/diet/internal/html.d', 'source/diet/internal/string.d', 'source/diet/parser.d', 'source/diet/traits.d', ] src_dir = include_directories('source/') # # Targets # diet_lib = library('diet', [diet_src], include_directories: [src_dir], install: true, version: project_version, soversion: project_soversion ) pkgc.generate(name: 'diet', libraries: [diet_lib], subdirs: 'd/diet', version: project_version, description: 'Next generation Diet template compiler.' ) # for use by Vibe.d and others which embed this as subproject diet_dep = declare_dependency( link_with: [diet_lib], include_directories: [src_dir] ) # # Tests # diet_test_exe = executable('test_diet', [diet_src], include_directories: [src_dir], d_args: meson.get_compiler('d').unittest_args(), link_args: '-main' ) test('test_diet', diet_test_exe) # # Install # install_subdir('source/diet/', install_dir: 'include/d/diet/') diet-ng-1.8.1/examples/0000755000175000017500000000000014230473643014222 5ustar nileshnileshdiet-ng-1.8.1/examples/htmlserver/0000755000175000017500000000000014230473643016415 5ustar nileshnileshdiet-ng-1.8.1/examples/htmlserver/source/0000755000175000017500000000000014230473643017715 5ustar nileshnileshdiet-ng-1.8.1/examples/htmlserver/source/app.d0000644000175000017500000000072714230473643020650 0ustar nileshnileshimport diet.html; import vibe.core.core; import vibe.http.server; import vibe.stream.wrapper; void render(scope HTTPServerRequest req, scope HTTPServerResponse res) { auto dst = streamOutputRange(res.bodyWriter); int iterations = 10; dst.compileHTMLDietFile!("index.dt", iterations); } void main() { auto settings = new HTTPServerSettings; settings.bindAddresses = ["::1", "127.0.0.1"]; settings.port = 8080; listenHTTP(settings, &render); runApplication(); } diet-ng-1.8.1/examples/htmlserver/README.md0000644000175000017500000000274714230473643017706 0ustar nileshnilesh# HTML Server Example In this subdirectory, you will find an example HTML server. There are several configurations you can run to see how they work. ## Normal mode If you just run with `dub`, you will get the standard diet-ng build for the templates. This build uses the diet-ng compiler to build the dynamically generated code. Changing the view template during runtime does not alter the produced page, as the view file is not reprocessed. ## Live Mode Using `dub --config=dietLive` will build this project in "live" mode. In live mode, changes to the template only related to HTML will be rendered without a recompile. Any string interpolations or D code escapes must remain the same, as changing these items would require a recompile of the server. Note that live mode is intended for development purposes to avoid a full compile cycle for front-end changes. It will not perform as well as the normal build, and it requires access to the views directory to display any templates. ## Cached mode Using `dub --config=dietCache` will build this project in "cached" mode. On the first execution of this build, the mixin generated by the compiler is cached inside the views directory. Upon supsequent builds, as long as the template source has not changed, the cached version is used. This is much faster as the library does not need to build the DOM of the diet file at compile time and process it. Please see the main README file inside the diet-ng project for more information about these features. diet-ng-1.8.1/examples/htmlserver/views/0000755000175000017500000000000014230473643017552 5ustar nileshnileshdiet-ng-1.8.1/examples/htmlserver/views/index.dt0000644000175000017500000000065414230473643021217 0ustar nileshnileshdoctype 5 html head title Hello, World body h1 Hello, World p Showing #{iterations} iterations: h2 Contents ul - foreach (i; 0 .. iterations) li: a(href="\#iteration-#{i+1}") Iteration #{i+1} - foreach (i; 0 .. iterations) h2(id="iteration-#{i+1}") Iteration #{i+1} p This is one of the iterations. - if (i+1 < iterations) p You can also go to the #[a(href="\#iteration-#{i+2}") next one]. diet-ng-1.8.1/examples/htmlserver/dub.sdl0000644000175000017500000000050014230473643017666 0ustar nileshnileshname "htmlserver" dependency "diet-ng" path="../.." dependency "vibe-d" version="~>0.9.0" configuration "application" { targetType "executable" } configuration "dietLive" { targetType "executable" versions "DietUseLive" } configuration "dietCache" { targetType "executable" versions "DietUseCache" } diet-ng-1.8.1/examples/htmlgenerator/0000755000175000017500000000000014230473643017075 5ustar nileshnileshdiet-ng-1.8.1/examples/htmlgenerator/source/0000755000175000017500000000000014230473643020375 5ustar nileshnileshdiet-ng-1.8.1/examples/htmlgenerator/source/app.d0000644000175000017500000000035414230473643021324 0ustar nileshnileshimport diet.html; import std.stdio; void main() { auto file = File("index.html", "wt"); auto dst = file.lockingTextWriter; int iterations = 10; dst.compileHTMLDietFile!("index.dt", iterations); writeln("Generated index.html."); } diet-ng-1.8.1/examples/htmlgenerator/views/0000755000175000017500000000000014230473643020232 5ustar nileshnileshdiet-ng-1.8.1/examples/htmlgenerator/views/index.dt0000644000175000017500000000065414230473643021677 0ustar nileshnileshdoctype 5 html head title Hello, World body h1 Hello, World p Showing #{iterations} iterations: h2 Contents ul - foreach (i; 0 .. iterations) li: a(href="\#iteration-#{i+1}") Iteration #{i+1} - foreach (i; 0 .. iterations) h2(id="iteration-#{i+1}") Iteration #{i+1} p This is one of the iterations. - if (i+1 < iterations) p You can also go to the #[a(href="\#iteration-#{i+2}") next one]. diet-ng-1.8.1/examples/htmlgenerator/dub.sdl0000644000175000017500000000006714230473643020356 0ustar nileshnileshname "htmlgenerator" dependency "diet-ng" path="../.." diet-ng-1.8.1/LICENSE.txt0000644000175000017500000000204514230473643014230 0ustar nileshnileshCopyright (c) 2012-2014 Sönke Ludwig Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.diet-ng-1.8.1/source/0000755000175000017500000000000014230473643013704 5ustar nileshnileshdiet-ng-1.8.1/source/diet/0000755000175000017500000000000014230473643014631 5ustar nileshnileshdiet-ng-1.8.1/source/diet/defs.d0000644000175000017500000000170414230473643015721 0ustar nileshnilesh/** Contains common types and constants. */ module diet.defs; import diet.dom; /** The name of the output range variable within a Diet template. D statements can access the variable with this name to directly write to the output. */ enum dietOutputRangeName = "_diet_output"; /// Thrown by the parser for malformed input. alias DietParserException = Exception; /** Throws an exception if the condition evaluates to `false`. This function will generate a proper error message including file and line number when called at compile time. An assertion is used in this case instead of an exception: Throws: Throws a `DietParserException` when called with a `false` condition at run time. */ void enforcep(bool cond, lazy string text, in Location loc) @safe { if (__ctfe) { import std.conv : to; assert(cond, loc.file~"("~(loc.line+1).to!string~"): "~text); } else { if (!cond) throw new DietParserException(text, loc.file, loc.line+1); } } diet-ng-1.8.1/source/diet/internal/0000755000175000017500000000000014230473643016445 5ustar nileshnileshdiet-ng-1.8.1/source/diet/internal/string.d0000644000175000017500000000321414230473643020120 0ustar nileshnileshmodule diet.internal.string; import std.ascii : isWhite; pure @safe nothrow: string ctstrip(string s) { size_t strt = 0, end = s.length; while (strt < s.length && s[strt].isWhite) strt++; while (end > 0 && s[end-1].isWhite) end--; return strt < end ? s[strt .. end] : null; } string ctstripLeft(string s) { size_t i = 0; while (i < s.length && s[i].isWhite) i++; return s[i .. $]; } string ctstripRight(string s) { size_t i = s.length; while (i > 0 && s[i-1].isWhite) i--; return s[0 .. i]; } string dstringEscape(char ch) { switch (ch) { default: return ""~ch; case '\\': return "\\\\"; case '\r': return "\\r"; case '\n': return "\\n"; case '\t': return "\\t"; case '\"': return "\\\""; } } string dstringEscape(in string str) { string ret; foreach( ch; str ) ret ~= dstringEscape(ch); return ret; } string dstringUnescape(in string str) { string ret; size_t i, start = 0; for( i = 0; i < str.length; i++ ) if( str[i] == '\\' ){ if( i > start ){ if( start > 0 ) ret ~= str[start .. i]; else ret = str[0 .. i]; } assert(i+1 < str.length, "The string ends with the escape char: " ~ str); switch(str[i+1]){ default: ret ~= str[i+1]; break; case 'r': ret ~= '\r'; break; case 'n': ret ~= '\n'; break; case 't': ret ~= '\t'; break; } i++; start = i+1; } if( i > start ){ if( start == 0 ) return str; else ret ~= str[start .. i]; } return ret; } string sanitizeEscaping(string str) { str = dstringUnescape(str); return dstringEscape(str); } string stripUTF8BOM(string input) { if (input.length >= 3 && input[0 .. 3] == [0xEF, 0xBB, 0xBF]) return input[3 .. $]; return input; } diet-ng-1.8.1/source/diet/internal/html.d0000644000175000017500000001372614230473643017567 0ustar nileshnilesh/** HTML character entity escaping - taken from the vibe.d project. TODO: Make things @safe once Appender is. Copyright: © 2012-2014 RejectedSoftware e.K. License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Sönke Ludwig */ module diet.internal.html; import std.array; import std.conv; import std.range; /** Returns the HTML escaped version of a given string. */ string htmlEscape(R)(R str) if (isInputRange!R) { if (__ctfe) { // appender is a performance/memory hog in ctfe StringAppender dst; filterHTMLEscape(dst, str); return dst.data; } else { auto dst = appender!string(); filterHTMLEscape(dst, str); return dst.data; } } /// unittest { assert(htmlEscape(`"Hello", !`) == `"Hello", <World>!`); } /** Writes the HTML escaped version of a given string to an output range. */ void filterHTMLEscape(R, S)(ref R dst, S str, HTMLEscapeFlags flags = HTMLEscapeFlags.escapeNewline) if (isOutputRange!(R, dchar) && isInputRange!S) { for (;!str.empty;str.popFront()) filterHTMLEscape(dst, str.front, flags); } /** Returns the HTML escaped version of a given string (also escapes double quotes). */ string htmlAttribEscape(R)(R str) if (isInputRange!R) { if (__ctfe) { // appender is a performance/memory hog in ctfe StringAppender dst; filterHTMLAttribEscape(dst, str); return dst.data; } else { auto dst = appender!string(); filterHTMLAttribEscape(dst, str); return dst.data; } } /// unittest { assert(htmlAttribEscape(`"Hello", !`) == `"Hello", <World>!`); } /** Writes the HTML escaped version of a given string to an output range (also escapes double quotes). */ void filterHTMLAttribEscape(R, S)(ref R dst, S str) if (isOutputRange!(R, dchar) && isInputRange!S) { for (; !str.empty; str.popFront()) filterHTMLEscape(dst, str.front, HTMLEscapeFlags.escapeNewline|HTMLEscapeFlags.escapeQuotes); } /** Returns the HTML escaped version of a given string (escapes every character). */ string htmlAllEscape(R)(R str) if (isInputRange!R) { if (__ctfe) { // appender is a performance/memory hog in ctfe StringAppender dst; filterHTMLAllEscape(dst, str); return dst.data; } else { auto dst = appender!string(); filterHTMLAllEscape(dst, str); return dst.data; } } /// unittest { assert(htmlAllEscape("Hello!") == "Hello!"); } /** Writes the HTML escaped version of a given string to an output range (escapes every character). */ void filterHTMLAllEscape(R, S)(ref R dst, S str) if (isOutputRange!(R, dchar) && isInputRange!S) { for (; !str.empty; str.popFront()) { dst.put("&#"); dst.put(to!string(cast(uint)str.front)); dst.put(';'); } } /** Minimally escapes a text so that no HTML tags appear in it. */ string htmlEscapeMin(R)(R str) if (isInputRange!R) { auto dst = appender!string(); for (; !str.empty; str.popFront()) filterHTMLEscape(dst, str.front, HTMLEscapeFlags.escapeMinimal); return dst.data(); } void htmlEscape(R, T)(ref R dst, T val) { import std.format : formattedWrite; auto r = HTMLEscapeOutputRange!R(dst, HTMLEscapeFlags.defaults); () @trusted { return &r; } ().formattedWrite("%s", val); } @safe unittest { static struct R { @safe @nogc nothrow: void put(char) {} void put(dchar) {} void put(in char[]) {}} R r; r.htmlEscape("foo"); r.htmlEscape(12); r.htmlEscape(12.4); } void htmlAttribEscape(R, T)(ref R dst, T val) { import std.format : formattedWrite; auto r = HTMLEscapeOutputRange!R(dst, HTMLEscapeFlags.attribute); () @trusted { return &r; } ().formattedWrite("%s", val); } /** Writes the HTML escaped version of a character to an output range. */ void filterHTMLEscape(R)(ref R dst, dchar ch, HTMLEscapeFlags flags = HTMLEscapeFlags.escapeNewline ) { switch (ch) { default: if (flags & HTMLEscapeFlags.escapeUnknown) { dst.put("&#"); dst.put(to!string(cast(uint)ch)); dst.put(';'); } else dst.put(ch); break; case '"': if (flags & HTMLEscapeFlags.escapeQuotes) dst.put("""); else dst.put('"'); break; case '\'': if (flags & HTMLEscapeFlags.escapeQuotes) dst.put("'"); else dst.put('\''); break; case '\r', '\n': if (flags & HTMLEscapeFlags.escapeNewline) { dst.put("&#"); dst.put(to!string(cast(uint)ch)); dst.put(';'); } else dst.put(ch); break; case 'a': .. case 'z': goto case; case 'A': .. case 'Z': goto case; case '0': .. case '9': goto case; case ' ', '\t', '-', '_', '.', ':', ',', ';', '#', '+', '*', '?', '=', '(', ')', '/', '!', '%' , '{', '}', '[', ']', '`', '´', '$', '^', '~': dst.put(cast(char)ch); break; case '<': dst.put("<"); break; case '>': dst.put(">"); break; case '&': dst.put("&"); break; } } enum HTMLEscapeFlags { escapeMinimal = 0, escapeQuotes = 1<<0, escapeNewline = 1<<1, escapeUnknown = 1<<2, defaults = escapeNewline, attribute = escapeNewline|escapeQuotes } private struct HTMLEscapeOutputRange(R) { R* dst; HTMLEscapeFlags flags; char[4] u8seq; uint u8seqfill; this(ref R dst, HTMLEscapeFlags flags) @safe nothrow @nogc { () @trusted { this.dst = &dst; } (); this.flags = flags; } @disable this(this); void put(char ch) { import std.utf : stride; assert(u8seqfill < u8seq.length); u8seq[u8seqfill++] = ch; if (u8seqfill >= 4 || stride(u8seq) <= u8seqfill) { char[] str = u8seq[0 .. u8seqfill]; put(u8seq[0 .. u8seqfill]); u8seqfill = 0; } } void put(dchar ch) { filterHTMLEscape(*dst, ch, flags); } void put(in char[] str) { foreach (dchar ch; str) put(ch); } static assert(isOutputRange!(HTMLEscapeOutputRange, char)); } unittest { // issue #36 auto dst = appender!string(); auto rng = HTMLEscapeOutputRange!(typeof(dst))(dst, HTMLEscapeFlags.attribute); rng.put("foo\"bar"); assert(dst.data == "foo"bar"); } private struct StringAppender { string data; @safe: void put(string s) { data ~= s; } void put(char ch) { data ~= ch; } void put(dchar ch) { import std.utf; char[4] dst; data ~= dst[0 .. encode(dst, ch)]; } } diet-ng-1.8.1/source/diet/dom.d0000644000175000017500000003341014230473643015556 0ustar nileshnilesh/** Types to represent the DOM tree. The DOM tree is used as an intermediate representation between the parser and the generator. Filters and other kinds of transformations can be executed on the DOM tree. The generator itself will apply filters and other traits using `diet.traits.applyTraits`. */ module diet.dom; import diet.internal.string; @safe: string expectText(const(Attribute) att) { import diet.defs : enforcep; if (att.contents.length == 0) return null; enforcep(att.isText, "'"~att.name~"' expected to be a pure text attribute.", att.loc); return att.contents[0].value; } string expectText(const(Node) n) { import diet.defs : enforcep; if (n.contents.length == 0) return null; enforcep(n.contents.length > 0 && n.contents[0].kind == NodeContent.Kind.text && (n.contents.length == 1 || n.contents[1].kind != NodeContent.Kind.node), "Expected pure text node.", n.loc); return n.contents[0].value; } string expectExpression(const(Attribute) att) { import diet.defs : enforcep; enforcep(att.isExpression, "'"~att.name~"' expected to be an expression attribute.", att.loc); return att.contents[0].value; } Node[] clone(in Node[] nodes) { auto ret = new Node[](nodes.length); foreach (i, ref n; ret) n = nodes[i].clone; return ret; } bool isExpression(const(Attribute) att) { return att.contents.length == 1 && att.contents[0].kind == AttributeContent.Kind.interpolation; } bool isText(const(Attribute) att) { return att.contents.length == 0 || att.contents.length == 1 && att.contents[0].kind == AttributeContent.Kind.text; } /** Converts an array of attribute contents to node contents. */ NodeContent[] toNodeContent(in AttributeContent[] contents, Location loc) { auto ret = new NodeContent[](contents.length); foreach (i, ref c; contents) { final switch (c.kind) { case AttributeContent.Kind.text: ret[i] = NodeContent.text(c.value, loc); break; case AttributeContent.Kind.interpolation: ret[i] = NodeContent.interpolation(c.value, loc); break; case AttributeContent.Kind.rawInterpolation: ret[i] = NodeContent.rawInterpolation(c.value, loc); break; } } return ret; } /** Encapsulates a full Diet template document. */ /*final*/ class Document { // non-final because of https://issues.dlang.org/show_bug.cgi?id=17146 Node[] nodes; this(Node[] nodes) { this.nodes = nodes; } } /** Represents a single node in the DOM tree. */ /*final*/ class Node { // non-final because of https://issues.dlang.org/show_bug.cgi?id=17146 @safe nothrow: /// A set of names that identify special-purpose nodes enum SpecialName { /** Normal comment. The content will appear in the output if the output format supports comments. */ comment = "//", /** Hidden comment. The content will never appear in the output. */ hidden = "//-", /** D statement. A node that has pure text as its first content, optionally followed by any number of child nodes. The text content is either a complete D statement, or an open block statement (without a block statement appended). In the latter case, all nested nodes are considered to be part of the block statement's body by the generator. */ code = "-", /** A dummy node that contains only text and string interpolations. These nodes behave the same as if their node content would be inserted in their place, except that they will cause whitespace (usually a space or a newline) to be prepended in the output, if they are not the first child of their parent. */ text = "|", /** Filter node. These nodes contain only text and string interpolations and have a "filterChain" attribute that contains a space separated list of filter names that are applied in reverse order when the traits (see `diet.traits.applyTraits`) are applied by the generator. */ filter = ":" } /// Start location of the node in the source file. Location loc; /// Name of the node string name; /// A key-value set of attributes. Attribute[] attributes; /// The main contents of the node. NodeContent[] contents; /// Flags that control the parser and generator behavior. NodeAttribs attribs; /// Original text used to look up the translation (only set if translated) string translationKey; /// Constructs a new node. this(Location loc = Location.init, string name = null, Attribute[] attributes = null, NodeContent[] contents = null, NodeAttribs attribs = NodeAttribs.none, string translation_key = null) { this.loc = loc; this.name = name; this.attributes = attributes; this.contents = contents; this.attribs = attribs; this.translationKey = translation_key; } /// Returns the "id" attribute. @property inout(Attribute) id() inout { return getAttribute("id"); } /// Returns "class" attribute - a white space separated list of style class identifiers. @property inout(Attribute) class_() inout { return getAttribute("class"); } Node clone() const { auto ret = new Node(this.loc, this.name, null, null, this.attribs, this.translationKey); ret.attributes.length = this.attributes.length; foreach (i, ref a; ret.attributes) a = this.attributes[i].dup; ret.contents.length = this.contents.length; foreach (i, ref c; ret.contents) c = this.contents[i].clone; return ret; } /** Adds a piece of text to the node's contents. If the node already has some content and the last piece of content is also text, with a matching location, the text will be appended to that `NodeContent`'s value. Otherwise, a new `NodeContent` will be appended. Params: text = The text to append to the node loc = Location in the source file */ void addText(string text, in Location loc) { if (contents.length && contents[$-1].kind == NodeContent.Kind.text && contents[$-1].loc == loc) contents[$-1].value ~= text; else contents ~= NodeContent.text(text, loc); } /** Removes all content if it conists of only white space. */ void stripIfOnlyWhitespace() { if (!this.hasNonWhitespaceContent) contents = null; } /** Determines if this node has any non-whitespace contents. */ bool hasNonWhitespaceContent() const { import std.algorithm.searching : any; return contents.any!(c => c.kind != NodeContent.Kind.text || c.value.ctstrip.length > 0); } /** Strips any leading whitespace from the contents. */ void stripLeadingWhitespace() { while (contents.length >= 1 && contents[0].kind == NodeContent.Kind.text) { contents[0].value = ctstripLeft(contents[0].value); if (contents[0].value.length == 0) contents = contents[1 .. $]; else break; } } /** Strips any trailign whitespace from the contents. */ void stripTrailingWhitespace() { while (contents.length >= 1 && contents[$-1].kind == NodeContent.Kind.text) { contents[$-1].value = ctstripRight(contents[$-1].value); if (contents[$-1].value.length == 0) contents = contents[0 .. $-1]; else break; } } /// Tests if the node consists of only a single, static string. bool isTextNode() const { return contents.length == 1 && contents[0].kind == NodeContent.Kind.text; } /// Tests if the node consists only of text and interpolations, but doesn't contain child nodes. bool isProceduralTextNode() const { import std.algorithm.searching : all; return contents.all!(c => c.kind != NodeContent.Kind.node); } bool hasAttribute(string name) const { foreach (ref a; this.attributes) if (a.name == name) return true; return false; } /** Returns a given named attribute. If the attribute doesn't exist, an empty value will be returned. */ inout(Attribute) getAttribute(string name) inout @trusted { foreach (ref a; this.attributes) if (a.name == name) return a; return cast(inout)Attribute(this.loc, name, null); } void setAttribute(Attribute att) { foreach (ref da; attributes) if (da.name == att.name) { da = att; return; } attributes ~= att; } /// Outputs a simple string representation of the node. override string toString() const { scope (failure) assert(false); import std.string : format; return format("Node(%s, \"%s\", %s, %s, %s, \"%s\")", this.tupleof); } /// Compares all properties of two nodes for equality. override bool opEquals(Object other_) { auto other = cast(Node)other_; if (!other) return false; return this.opEquals(other); } bool opEquals(scope const Node other) scope const { return this.tupleof == other.tupleof; } } /** Flags that control parser or generator behavior. */ enum NodeAttribs { none = 0, translated = 1<<0, /// Translate node contents textNode = 1<<1, /// All nested lines are treated as text rawTextNode = 1<<2, /// All nested lines are treated as raw text (no interpolations or inline tags) fitOutside = 1<<3, /// Don't insert white space outside of the node when generating output (currently ignored by the HTML generator) fitInside = 1<<4, /// Don't insert white space around the node contents when generating output (currently ignored by the HTML generator) } /** A single node attribute. Attributes are key-value pairs, where the value can either be empty (considered as a Boolean value of `true`), a string with optional string interpolations, or a D expression (stored as a single `interpolation` `AttributeContent`). */ struct Attribute { @safe nothrow: /// Location in source file Location loc; /// Name of the attribute string name; /// Value of the attribute AttributeContent[] contents; /// Creates a new attribute with a static text value. static Attribute text(string name, string value, Location loc) { return Attribute(loc, name, [AttributeContent.text(value)]); } /// Creates a new attribute with an expression based value. static Attribute expr(string name, string value, Location loc) { return Attribute(loc, name, [AttributeContent.interpolation(value)]); } this(Location loc, string name, AttributeContent[] contents) { this.name = name; this.contents = contents; this.loc = loc; } /// Creates a copy of the attribute. @property Attribute dup() const { return Attribute(loc, name, contents.dup); } /** Appends raw text to the attribute. If the attribute already has contents and the last piece of content is also text, then the text will be appended to the value of that `AttributeContent`. Otherwise, a new `AttributeContent` will be appended to `contents`. */ void addText(string str) { if (contents.length && contents[$-1].kind == AttributeContent.Kind.text) contents[$-1].value ~= str; else contents ~= AttributeContent.text(str); } /** Appends a list of contents. If the list of contents starts with a text `AttributeContent`, then this first part will be appended using the same rules as for `addText`. The remaining parts will be appended normally. */ void addContents(const(AttributeContent)[] contents) { if (contents.length > 0 && contents[0].kind == AttributeContent.Kind.text) { addText(contents[0].value); contents = contents[1 .. $]; } this.contents ~= contents; } } /** A single piece of an attribute value. */ struct AttributeContent { @safe nothrow: /// enum Kind { text, /// Raw text (will be escaped by the generator as necessary) interpolation, /// A D expression that will be converted to text at runtime (escaped as necessary) rawInterpolation /// A D expression that will be converted to text at runtime (not escaped) } /// Kind of this attribute content Kind kind; /// The value - either text or a D expression string value; /// Creates a new text attribute content value. static AttributeContent text(string text) { return AttributeContent(Kind.text, text); } /// Creates a new string interpolation attribute content value. static AttributeContent interpolation(string expression) { return AttributeContent(Kind.interpolation, expression); } /// Creates a new raw string interpolation attribute content value. static AttributeContent rawInterpolation(string expression) { return AttributeContent(Kind.rawInterpolation, expression); } } /** A single piece of node content. */ struct NodeContent { @safe nothrow: /// enum Kind { node, /// A child node text, /// Raw text (not escaped in the output) interpolation, /// A D expression that will be converted to text at runtime (escaped as necessary) rawInterpolation /// A D expression that will be converted to text at runtime (not escaped) } /// Kind of this node content Kind kind; /// Location of the content in the source file Location loc; /// The node - only used for `Kind.node` Node node; /// The string value - either text or a D expression string value; /// Creates a new child node content value. static NodeContent tag(Node node) { return NodeContent(Kind.node, node.loc, node); } /// Creates a new text node content value. static NodeContent text(string text, Location loc) { return NodeContent(Kind.text, loc, Node.init, text); } /// Creates a new string interpolation node content value. static NodeContent interpolation(string text, Location loc) { return NodeContent(Kind.interpolation, loc, Node.init, text); } /// Creates a new raw string interpolation node content value. static NodeContent rawInterpolation(string text, Location loc) { return NodeContent(Kind.rawInterpolation, loc, Node.init, text); } @property NodeContent clone() const { NodeContent ret; ret.kind = this.kind; ret.loc = this.loc; ret.value = this.value; if (this.node) ret.node = this.node.clone; return ret; } /// Compares node content for equality. bool opEquals(const scope ref NodeContent other) const scope { if (this.kind != other.kind) return false; if (this.loc != other.loc) return false; if (this.value != other.value) return false; if (this.node is other.node) return true; if (this.node is null || other.node is null) return false; return this.node.opEquals(other.node); } } /// Represents the location of an entity within the source file. struct Location { /// Name of the source file string file; /// Zero based line index within the file int line; } diet-ng-1.8.1/source/diet/traits.d0000644000175000017500000002511614230473643016311 0ustar nileshnilesh/** Definitions to support customization of the Diet compilation process. */ module diet.traits; import diet.dom; /** Marks a struct as a Diet traits container. A traits struct can contain any of the following: $(UL $(LI `string translate(string)` - A function that takes a `string` and returns the translated version of that string. This is used for translating the text of nodes marked with `&` at compile time. Note that the input string may contain string interpolations.) $(LI `void filterX(string)` - Any number of compile-time filter functions, where "X" is a placeholder for the actual filter name. The first character will be converted to lower case, so that a function `filterCss` will be available as `:css` within the Diet template.) $(LI `SafeFilterCallback[string] filters` - A dictionary of runtime filter functions that will be used to for filter nodes that don't have an available compile-time filter or contain string interpolations.) $(LI `alias processors = AliasSeq!(...)` - A list of callables taking a `Document` to modify its contents) $(LI `HTMLOutputStyle htmlOutputStyle` - An enum to configure the output style of the generated HTML, e.g. compact or pretty) ) */ @property DietTraitsAttribute dietTraits() @safe { return DietTraitsAttribute.init; } /// @safe unittest { import diet.html : compileHTMLDietString; import std.array : appender, array; import std.string : toUpper; @dietTraits static struct CTX { static string translate(string text) { return text == "Hello, World!" ? "Hallo, Welt!" : text; } static string filterUppercase(I)(I input) { return input.toUpper(); } } auto dst = appender!string; dst.compileHTMLDietString!("p& Hello, World!", CTX); assert(dst.data == "

Hallo, Welt!

"); dst = appender!string; dst.compileHTMLDietString!(":uppercase testing", CTX); assert(dst.data == "TESTING"); } /** Translates a line of text based on the traits passed to the Diet parser. The input text may contain string interpolations of the form `#{...}` or `!{...}`, where the contents form an arbitrary D expression. The translation function is required to pass these through unmodified. */ string translate(TRAITS...)(string text, string context = null) { import std.traits : hasUDA; foreach (T; TRAITS) { static assert(hasUDA!(T, DietTraitsAttribute)); static if (is(typeof(&T.translate))) { static if (is(typeof(T.translate(text, context)))) text = T.translate(text, context); else text = T.translate(text); } } return text; } /** Applies any transformations that are defined in the supplied traits list. Transformations are defined by declaring a `processors` sequence in a traits struct. See_also: `dietTraits` */ Document applyTraits(TRAITS...)(Document doc) { import diet.defs : enforcep; import std.algorithm.searching : startsWith; import std.array : split; void processNode(ref Node n, bool in_filter) { bool is_filter = n.name == Node.SpecialName.filter; // process children first for (size_t i = 0; i < n.contents.length;) { auto nc = n.contents[i]; if (nc.kind == NodeContent.Kind.node) { processNode(nc.node, is_filter || in_filter); if ((is_filter || in_filter) && nc.node.name == Node.SpecialName.text) { n.contents = n.contents[0 .. i] ~ nc.node.contents ~ n.contents[i+1 .. $]; i += nc.node.contents.length; } else i++; } else i++; } // then consolidate text for (size_t i = 1; i < n.contents.length;) { if (n.contents[i-1].kind == NodeContent.Kind.text && n.contents[i].kind == NodeContent.Kind.text) { n.contents[i-1].value ~= n.contents[i].value; n.contents = n.contents[0 .. i] ~ n.contents[i+1 .. $]; } else i++; } // finally process filters if (is_filter) { enforcep(n.isProceduralTextNode, "Only text is supported as filter contents.", n.loc); auto chain = n.getAttribute("filterChain").expectText().split(' '); n.attributes = null; n.attribs = NodeAttribs.none; if (n.isTextNode) { while (chain.length) { if (hasFilterCT!TRAITS(chain[$-1])) { n.contents[0].value = runFilterCT!TRAITS(n.contents[0].value, chain[$-1]); chain.length--; } else break; } } if (!chain.length) n.name = Node.SpecialName.text; else { n.name = Node.SpecialName.code; n.contents = [NodeContent.text(generateFilterChainMixin(chain, n.contents), n.loc)]; } } } foreach (ref n; doc.nodes) processNode(n, false); // apply DOM processors foreach (T; TRAITS) { static if (is(typeof(T.processors.length))) { foreach (p; T.processors) p(doc); } } return doc; } deprecated("Use SafeFilterCallback instead.") alias FilterCallback = void delegate(in char[] input, scope CharacterSink output); alias SafeFilterCallback = void delegate(in char[] input, scope CharacterSink output) @safe; alias CharacterSink = void delegate(in char[]) @safe; void filter(ALIASES...)(in char[] input, string filter, CharacterSink output) { import std.traits : hasUDA; foreach (A; ALIASES) static if (hasUDA!(A, DietTraitsAttribute)) { static if (is(typeof(A.filters))) if (auto pf = filter in A.filters) { (*pf)(input, output); return; } } // FIXME: output location information throw new Exception("Unknown filter: "~filter); } private string generateFilterChainMixin(string[] chain, NodeContent[] contents) @safe { import std.format : format; import diet.defs : enforcep, dietOutputRangeName; import diet.internal.string : dstringEscape; string ret = `{ import std.array : appender; import std.format : formattedWrite; `; auto tloname = format("__f%s", chain.length); if (contents.length == 1 && contents[0].kind == NodeContent.Kind.text) { ret ~= q{enum %s = "%s";}.format(tloname, dstringEscape(contents[0].value)); } else { ret ~= q{auto %s_app = appender!(char[])();}.format(tloname); foreach (c; contents) { switch (c.kind) { default: assert(false, "Unexpected node content in filter."); case NodeContent.Kind.text: ret ~= q{%s_app.put("%s");}.format(tloname, dstringEscape(c.value)); break; case NodeContent.Kind.rawInterpolation: ret ~= q{%s_app.formattedWrite("%%s", %s);}.format(tloname, c.value); break; case NodeContent.Kind.interpolation: enforcep(false, "Non-raw interpolations are not supported within filter contents.", c.loc); break; } ret ~= "\n"; } ret ~= q{auto %s = %s_app.data;}.format(tloname, tloname); } foreach_reverse (i, f; chain) { ret ~= "\n"; string iname = format("__f%s", i+1); string oname; if (i > 0) { oname = format("__f%s_app", i); ret ~= q{auto %s = appender!(char[]);}.format(oname); } else oname = dietOutputRangeName; ret ~= q{%s.filter!ALIASES("%s", (in char[] s) @safe { %s.put(s); });}.format(iname, dstringEscape(f), oname); if (i > 0) ret ~= q{auto __f%s = %s.data;}.format(i, oname); } return ret ~ `}`; } @safe unittest { import std.array : appender; import diet.html : compileHTMLDietString; @dietTraits static struct CTX { static string filterFoo(string str) { return "("~str~")"; } static SafeFilterCallback[string] filters; } 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)"); dst = appender!string; dst.compileHTMLDietString!(":foo text !{1}", CTX); assert(dst.data == "(Rtext 1R)"); } @safe unittest { import diet.html : compileHTMLDietString; static struct R { void put(char) @safe {} void put(in char[]) @safe {} void put(dchar) @safe {} } @dietTraits static struct CTX { static SafeFilterCallback[string] filters; } CTX.filters["foo"] = (input, scope output) { output(input); }; R r; r.compileHTMLDietString!(":foo bar", CTX); } package struct DietTraitsAttribute {} private bool hasFilterCT(TRAITS...)(string filter) { alias Filters = FiltersFromTraits!TRAITS; static if (Filters.length) { switch (filter) { default: break; foreach (i, F; Filters) { case FilterName!(Filters[i]): return true; } } } return false; } private string runFilterCT(TRAITS...)(string text, string filter) { alias Filters = FiltersFromTraits!TRAITS; static if (Filters.length) { switch (filter) { default: break; foreach (i, F; Filters) { case FilterName!(Filters[i]): return F(text); } } } return text; // FIXME: error out? } private template FiltersFromTraits(TRAITS...) { import std.meta : AliasSeq; template impl(size_t i) { static if (i < TRAITS.length) { // FIXME: merge lists avoiding duplicates alias impl = AliasSeq!(FiltersFromContext!(TRAITS[i]), impl!(i+1)); } else alias impl = AliasSeq!(); } alias FiltersFromTraits = impl!0; } /** Extracts all Diet traits structs from a set of aliases as passed to a render function. */ template DietTraits(ALIASES...) { import std.meta : AliasSeq; import std.traits : hasUDA; template impl(size_t i) { static if (i < ALIASES.length) { static if (is(ALIASES[i]) && hasUDA!(ALIASES[i], DietTraitsAttribute)) { alias impl = AliasSeq!(ALIASES[i], impl!(i+1)); } else alias impl = impl!(i+1); } else alias impl = AliasSeq!(); } alias DietTraits = impl!0; } private template FiltersFromContext(Context) { import std.meta : AliasSeq; import std.algorithm.searching : startsWith; alias members = AliasSeq!(__traits(allMembers, Context)); template impl(size_t i) { static if (i < members.length) { static if (members[i].startsWith("filter") && members[i].length > 6 && members[i] != "filters") alias impl = AliasSeq!(__traits(getMember, Context, members[i]), impl!(i+1)); else alias impl = impl!(i+1); } else alias impl = AliasSeq!(); } alias FiltersFromContext = impl!0; } private template FilterName(alias FilterFunction) { import std.algorithm.searching : startsWith; import std.ascii : toLower; enum ident = __traits(identifier, FilterFunction); static if (ident.startsWith("filter") && ident.length > 6) enum FilterName = ident[6].toLower ~ ident[7 .. $]; else static assert(false, "Filter function must start with \"filter\" and must have a non-zero length suffix: " ~ ident); } diet-ng-1.8.1/source/diet/input.d0000644000175000017500000001311314230473643016134 0ustar nileshnilesh/** Contains common definitions and logic to collect input dependencies. This module is typically only used by generator implementations. */ module diet.input; import diet.traits : DietTraitsAttribute; @safe: /** Converts a `Group` with alternating file names and contents to an array of `InputFile`s. */ @property InputFile[] filesFromGroup(alias FILES_GROUP)() { static assert(FILES_GROUP.expand.length % 2 == 0); auto ret = new InputFile[FILES_GROUP.expand.length / 2]; foreach (i, F; FILES_GROUP.expand) { static if (i % 2 == 0) { ret[i / 2].name = FILES_GROUP.expand[i+1]; ret[i / 2].contents = FILES_GROUP.expand[i]; } } return ret; } /** Using the file name of a string import Diet file, returns a list of all required files. These files recursively include all imports or extension templates that are used. The type of the list is `InputFile[]`. */ template collectFiles(string root_file) { import diet.internal.string : stripUTF8BOM; private static immutable contents = stripUTF8BOM(import(root_file)); enum collectFiles = collectFiles!(root_file, contents); } /// ditto template collectFiles(string root_file, alias root_contents) { import std.algorithm.searching : canFind; enum baseFiles = collectReferencedFiles!(root_file, root_contents); static if (baseFiles.canFind!(f => f.name == root_file)) enum collectFiles = baseFiles; else enum collectFiles = InputFile(root_file, root_contents) ~ baseFiles; } /// Encapsulates a single input file. struct InputFile { string name; string contents; } /** Helper template to aggregate a list of compile time values. This is similar to `AliasSeq`, but does not auto-expand. */ template Group(A...) { import std.typetuple; alias expand = TypeTuple!A; } /** Returns a mixin string that makes all passed symbols available in the mixin's scope. */ template localAliasesMixin(int i, ALIASES...) { import std.traits : hasUDA; static if (i < ALIASES.length) { import std.conv : to; static if (hasUDA!(ALIASES[i], DietTraitsAttribute)) enum string localAliasesMixin = localAliasesMixin!(i+1); else enum string localAliasesMixin = "alias ALIASES["~i.to!string~"] "~__traits(identifier, ALIASES[i])~";\n" ~localAliasesMixin!(i+1, ALIASES); } else { enum string localAliasesMixin = ""; } } private template collectReferencedFiles(string file_name, alias file_contents) { import std.path : extension; enum references = collectReferences(file_contents); template impl(size_t i) { static if (i < references.length) { enum rfiles = impl!(i+1); static if (__traits(compiles, import(references[i]))) { enum ifiles = collectFiles!(references[i]); enum impl = merge(ifiles, rfiles); } else static if (__traits(compiles, import(references[i] ~ extension(file_name)))) { enum ifiles = collectFiles!(references[i] ~ extension(file_name)); enum impl = merge(ifiles, rfiles); } else enum impl = rfiles; } else enum InputFile[] impl = []; } alias collectReferencedFiles = impl!0; } private string[] collectReferences(string content) { import std.string : strip, stripLeft, splitLines; import std.algorithm.searching : startsWith; string[] ret; foreach (i, ln; content.stripLeft().splitLines()) { // FIXME: this produces false-positives when a text paragraph is used: // p. // This is some text. // import oops, this is also just text. ln = ln.stripLeft(); if (i == 0 && ln.startsWith("extends ")) ret ~= ln[8 .. $].strip(); else if (ln.startsWith("include ")) ret ~= ln[8 .. $].strip(); } return ret; } private InputFile[] merge(InputFile[] a, InputFile[] b) { import std.algorithm.searching : canFind; auto ret = a; foreach (f; b) if (!a.canFind!(g => g.name == f.name)) ret ~= f; return ret; } /** Runtime equivalent of collectFiles. This version uses std.file to read files from the appropriate directory. Note that for collectFiles, the directory to use is passed on the command line, whereas in this case, we must receive the directory containing the files from the caller. Params: file = The root file that will be used to find all referenced files. source_directories = Optional base directory list from which all files will be searched. Returns: An array of InputFile structs containing the list of files that are referenced from the root file, and their contents. */ InputFile[] rtGetInputs(string file, string[] source_directories...) { // for each of the files, import the file, get all the references, and // continually import files until we have them all. import std.range; import std.file : readText, exists; import diet.internal.string : stripUTF8BOM; import std.algorithm : canFind; import std.path : buildPath, extension; if (!source_directories.length) source_directories = [""]; auto ext = extension(file); string[] filesToProcess = [file]; InputFile[] result; void addFile(string fname) { if (!filesToProcess.canFind(fname) && !result.canFind!(g => g.name == fname)) filesToProcess ~= fname; } next_file: while (filesToProcess.length) { auto nextFile = filesToProcess.front; filesToProcess.popFront(); foreach (dir; source_directories) { if (exists(buildPath(dir, nextFile))) { auto newInput = InputFile(nextFile, stripUTF8BOM(readText(buildPath(dir, nextFile)))); result ~= newInput; foreach (f; collectReferences(newInput.contents)) addFile(f); continue next_file; } if (exists(buildPath(dir, nextFile ~ ext))) { addFile(nextFile ~ ext); continue next_file; } } throw new Exception("Cannot find necessary file " ~ nextFile ~ " to parse strings for " ~ file); } assert(result.length > 0); return result; } diet-ng-1.8.1/source/diet/parser.d0000644000175000017500000014024014230473643016273 0ustar nileshnilesh/** Generic Diet format parser. Performs generic parsing of a Diet template file. The resulting AST is agnostic to the output format context in which it is used. Format specific constructs, such as inline code or special tags, are parsed as-is without any preprocessing. The supported features of the are: $(UL $(LI string interpolations) $(LI assignment expressions) $(LI blocks/extensions) $(LI includes) $(LI text paragraphs) $(LI translation annotations) $(LI class and ID attribute shortcuts) ) */ module diet.parser; import diet.dom; import diet.defs; import diet.input; import diet.internal.string; import std.algorithm.searching : endsWith, startsWith; import std.range.primitives : empty, front, popFront, popFrontN; version(unittest) { // this is needed to make unittests safe for comparison. Due to // Object.opCmp being @system, we cannot fix this here. bool nodeEq(Node[] arr1, Node[] arr2) @trusted { return arr1 == arr2; } } /** Parses a Diet template document and outputs the resulting DOM tree. The overload that takes a list of files will automatically resolve includes and extensions. Params: TR = An optional translation function that takes and returns a string. This function will be invoked whenever node text contents need to be translated at compile tile (for the `&` node suffix). text = For the single-file overload, specifies the contents of the Diet template. filename = For the single-file overload, specifies the file name that is displayed in error messages and stored in the DOM `Location`s. files = A full set of Diet template files. All files referenced in includes or extension directives must be present. Returns: The list of parsed root nodes is returned. */ Document parseDiet(alias TR = identity)(string text, string filename = "string") if (is(typeof(TR(string.init)) == string) || is(typeof(TR(string.init, string.init)) == string)) { InputFile[1] f; f[0].name = filename; f[0].contents = text; return parseDiet!TR(f); } /// Ditto Document parseDiet(alias TR = identity)(const(InputFile)[] files) if (is(typeof(TR(string.init)) == string) || is(typeof(TR(string.init, string.init)) == string)) { import diet.traits; import std.algorithm.iteration : map; import std.array : array; assert(files.length > 0, "Empty set of input files"); FileInfo[] parsed_files = files.map!(f => FileInfo(f.name, parseDietRaw!TR(f))).array; BlockInfo[] blocks; return new Document(parseDietWithExtensions(parsed_files, 0, blocks, null)); } @safe unittest { // test basic functionality Location ln(int l) @safe { return Location("string", l); } // simple node assert(parseDiet("test").nodes.nodeEq([ new Node(ln(0), "test") ])); // nested nodes assert(parseDiet("foo\n bar").nodes.nodeEq([ new Node(ln(0), "foo", null, [ NodeContent.tag(new Node(ln(1), "bar")) ]) ])); // node with id and classes assert(parseDiet("test#id.cls1.cls2").nodes.nodeEq([ new Node(ln(0), "test", [ Attribute(ln(0), "id", [AttributeContent.text("id")]), Attribute(ln(0), "class", [AttributeContent.text("cls1")]), Attribute(ln(0), "class", [AttributeContent.text("cls2")]) ]) ])); assert(parseDiet("test.cls1#id.cls2").nodes.nodeEq([ // issue #9 new Node(ln(0), "test", [ Attribute(ln(0), "class", [AttributeContent.text("cls1")]), Attribute(ln(0), "id", [AttributeContent.text("id")]), Attribute(ln(0), "class", [AttributeContent.text("cls2")]) ]) ])); // empty tag name (only class) assert(parseDiet(".foo").nodes.nodeEq([ new Node(ln(0), "", [ Attribute(ln(0), "class", [AttributeContent.text("foo")]) ]) ])); assert(parseDiet("a.download-button\n\t.bs-hbtn.right.black").nodes.nodeEq([ new Node(ln(0), "a", [ Attribute(ln(0), "class", [AttributeContent.text("download-button")]), ], [ NodeContent.tag(new Node(ln(1), "", [ Attribute(ln(1), "class", [AttributeContent.text("bs-hbtn")]), Attribute(ln(1), "class", [AttributeContent.text("right")]), Attribute(ln(1), "class", [AttributeContent.text("black")]) ])) ]) ])); // empty tag name (only id) assert(parseDiet("#foo").nodes.nodeEq([ new Node(ln(0), "", [ Attribute(ln(0), "id", [AttributeContent.text("foo")]) ]) ])); // node with attributes assert(parseDiet("test(foo1=\"bar\", foo2=2+3)").nodes.nodeEq([ new Node(ln(0), "test", [ Attribute(ln(0), "foo1", [AttributeContent.text("bar")]), Attribute(ln(0), "foo2", [AttributeContent.interpolation("2+3")]) ]) ])); // node with pure text contents assert(parseDiet("foo.\n\thello\n\t world").nodes.nodeEq([ new Node(ln(0), "foo", null, [ NodeContent.text("hello", ln(1)), NodeContent.text("\n world", ln(2)) ], NodeAttribs.textNode) ])); assert(parseDiet("foo.\n\thello\n\n\t world").nodes.nodeEq([ new Node(ln(0), "foo", null, [ NodeContent.text("hello", ln(1)), NodeContent.text("\n", ln(2)), NodeContent.text("\n world", ln(3)) ], NodeAttribs.textNode) ])); // translated text assert(parseDiet("foo& test").nodes.nodeEq([ new Node(ln(0), "foo", null, [ NodeContent.text("test", ln(0)) ], NodeAttribs.translated, "test") ])); // interpolated text assert(parseDiet("foo hello #{\"world\"} #bar \\#{baz}").nodes.nodeEq([ new Node(ln(0), "foo", null, [ NodeContent.text("hello ", ln(0)), NodeContent.interpolation(`"world"`, ln(0)), NodeContent.text(" #bar #{baz}", ln(0)) ]) ])); // expression assert(parseDiet("foo= 1+2").nodes.nodeEq([ new Node(ln(0), "foo", null, [ NodeContent.interpolation(`1+2`, ln(0)), ]) ])); // expression with empty tag name assert(parseDiet("= 1+2").nodes.nodeEq([ new Node(ln(0), "", null, [ NodeContent.interpolation(`1+2`, ln(0)), ]) ])); // raw expression assert(parseDiet("foo!= 1+2").nodes.nodeEq([ new Node(ln(0), "foo", null, [ NodeContent.rawInterpolation(`1+2`, ln(0)), ]) ])); // interpolated attribute text assert(parseDiet("foo(att='hello #{\"world\"} #bar')").nodes.nodeEq([ new Node(ln(0), "foo", [ Attribute(ln(0), "att", [ AttributeContent.text("hello "), AttributeContent.interpolation(`"world"`), AttributeContent.text(" #bar") ]) ]) ])); // attribute expression assert(parseDiet("foo(att=1+2)").nodes.nodeEq([ new Node(ln(0), "foo", [ Attribute(ln(0), "att", [ AttributeContent.interpolation(`1+2`), ]) ]) ])); // multiline attribute expression assert(parseDiet("foo(\n\tatt=1+2,\n\tfoo=bar\n)").nodes.nodeEq([ new Node(ln(0), "foo", [ Attribute(ln(0), "att", [ AttributeContent.interpolation(`1+2`), ]), Attribute(ln(0), "foo", [ AttributeContent.interpolation(`bar`), ]) ]) ])); // special nodes assert(parseDiet("//comment").nodes.nodeEq([ new Node(ln(0), Node.SpecialName.comment, null, [NodeContent.text("comment", ln(0))], NodeAttribs.rawTextNode) ])); assert(parseDiet("//-hide").nodes.nodeEq([ new Node(ln(0), Node.SpecialName.hidden, null, [NodeContent.text("hide", ln(0))], NodeAttribs.rawTextNode) ])); assert(parseDiet("!!! 5").nodes.nodeEq([ new Node(ln(0), "doctype", null, [NodeContent.text("5", ln(0))]) ])); assert(parseDiet("").nodes.nodeEq([ new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text("", ln(0))]) ])); assert(parseDiet("|text").nodes.nodeEq([ new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text("text", ln(0))]) ])); assert(parseDiet("|text\n").nodes.nodeEq([ new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text("text", ln(0))]) ])); assert(parseDiet("| text\n").nodes.nodeEq([ new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text("text", ln(0))]) ])); assert(parseDiet("|.").nodes.nodeEq([ new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text(".", ln(0))]) ])); assert(parseDiet("|:").nodes.nodeEq([ new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text(":", ln(0))]) ])); assert(parseDiet("|&x").nodes.nodeEq([ new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text("x", ln(0))], NodeAttribs.translated, "x") ])); assert(parseDiet("-if(x)").nodes.nodeEq([ new Node(ln(0), Node.SpecialName.code, null, [NodeContent.text("if(x)", ln(0))]) ])); assert(parseDiet("-if(x)\n\t|bar").nodes.nodeEq([ new Node(ln(0), Node.SpecialName.code, null, [ NodeContent.text("if(x)", ln(0)), NodeContent.tag(new Node(ln(1), Node.SpecialName.text, null, [ NodeContent.text("bar", ln(1)) ])) ]) ])); assert(parseDiet(":foo\n\tbar").nodes.nodeEq([ new Node(ln(0), ":", [Attribute(ln(0), "filterChain", [AttributeContent.text("foo")])], [ NodeContent.text("bar", ln(1)) ], NodeAttribs.textNode) ])); assert(parseDiet(":foo :bar baz").nodes.nodeEq([ new Node(ln(0), ":", [Attribute(ln(0), "filterChain", [AttributeContent.text("foo bar")])], [ NodeContent.text("baz", ln(0)) ], NodeAttribs.textNode) ])); assert(parseDiet(":foo\n\t:bar baz").nodes.nodeEq([ new Node(ln(0), ":", [Attribute(ln(0), "filterChain", [AttributeContent.text("foo")])], [ NodeContent.text(":bar baz", ln(1)) ], NodeAttribs.textNode) ])); assert(parseDiet(":foo\n\tbar\n\t\t:baz").nodes.nodeEq([ new Node(ln(0), ":", [Attribute(ln(0), "filterChain", [AttributeContent.text("foo")])], [ NodeContent.text("bar", ln(1)), NodeContent.text("\n\t:baz", ln(2)) ], NodeAttribs.textNode) ])); // nested nodes assert(parseDiet("a: b").nodes.nodeEq([ new Node(ln(0), "a", null, [ NodeContent.tag(new Node(ln(0), "b")) ]) ])); assert(parseDiet("a: b\n\tc\nd").nodes.nodeEq([ new Node(ln(0), "a", null, [ NodeContent.tag(new Node(ln(0), "b", null, [ NodeContent.tag(new Node(ln(1), "c")) ])) ]), new Node(ln(2), "d") ])); // inline nodes assert(parseDiet("a #[b]").nodes.nodeEq([ new Node(ln(0), "a", null, [ NodeContent.tag(new Node(ln(0), "b")) ]) ])); assert(parseDiet("a #[b #[c d]]").nodes.nodeEq([ new Node(ln(0), "a", null, [ NodeContent.tag(new Node(ln(0), "b", null, [ NodeContent.tag(new Node(ln(0), "c", null, [ NodeContent.text("d", ln(0)) ])) ])) ]) ])); // whitespace fitting assert(parseDiet("a<>").nodes.nodeEq([ new Node(ln(0), "a", null, [], NodeAttribs.fitInside|NodeAttribs.fitOutside) ])); assert(parseDiet("a><").nodes.nodeEq([ new Node(ln(0), "a", null, [], NodeAttribs.fitInside|NodeAttribs.fitOutside) ])); assert(parseDiet("a<").nodes.nodeEq([ new Node(ln(0), "a", null, [], NodeAttribs.fitInside) ])); assert(parseDiet("a>").nodes.nodeEq([ new Node(ln(0), "a", null, [], NodeAttribs.fitOutside) ])); } @safe unittest { Location ln(int l) { return Location("string", l); } // angular2 html attributes tests assert(parseDiet("div([value]=\"firstName\")").nodes.nodeEq([ new Node(ln(0), "div", [ Attribute(ln(0), "[value]", [ AttributeContent.text("firstName"), ]) ]) ])); assert(parseDiet("div([attr.role]=\"myRole\")").nodes.nodeEq([ new Node(ln(0), "div", [ Attribute(ln(0), "[attr.role]", [ AttributeContent.text("myRole"), ]) ]) ])); assert(parseDiet("div([attr.role]=\"{foo:myRole}\")").nodes.nodeEq([ new Node(ln(0), "div", [ Attribute(ln(0), "[attr.role]", [ AttributeContent.text("{foo:myRole}"), ]) ]) ])); assert(parseDiet("div([attr.role]=\"{foo:myRole, bar:MyRole}\")").nodes.nodeEq([ new Node(ln(0), "div", [ Attribute(ln(0), "[attr.role]", [ AttributeContent.text("{foo:myRole, bar:MyRole}") ]) ]) ])); assert(parseDiet("div((attr.role)=\"{foo:myRole, bar:MyRole}\")").nodes.nodeEq([ new Node(ln(0), "div", [ Attribute(ln(0), "(attr.role)", [ AttributeContent.text("{foo:myRole, bar:MyRole}") ]) ]) ])); assert(parseDiet("div([class.extra-sparkle]=\"isDelightful\")").nodes.nodeEq([ new Node(ln(0), "div", [ Attribute(ln(0), "[class.extra-sparkle]", [ AttributeContent.text("isDelightful") ]) ]) ])); auto t = parseDiet("div((click)=\"readRainbow($event)\")"); assert(t.nodes.nodeEq([ new Node(ln(0), "div", [ Attribute(ln(0), "(click)", [ AttributeContent.text("readRainbow($event)") ]) ]) ])); assert(parseDiet("div([(title)]=\"name\")").nodes.nodeEq([ new Node(ln(0), "div", [ Attribute(ln(0), "[(title)]", [ AttributeContent.text("name") ]) ]) ])); assert(parseDiet("div(*myUnless=\"myExpression\")").nodes.nodeEq([ new Node(ln(0), "div", [ Attribute(ln(0), "*myUnless", [ AttributeContent.text("myExpression") ]) ]) ])); assert(parseDiet("div([ngClass]=\"{active: isActive, disabled: isDisabled}\")").nodes.nodeEq([ new Node(ln(0), "div", [ Attribute(ln(0), "[ngClass]", [ AttributeContent.text("{active: isActive, disabled: isDisabled}") ]) ]) ])); t = parseDiet("div(*ngFor=\"\\#item of list\")"); assert(t.nodes.nodeEq([ new Node(ln(0), "div", [ Attribute(ln(0), "*ngFor", [ AttributeContent.text("#"), AttributeContent.text("item of list") ]) ]) ])); t = parseDiet("div(({*ngFor})=\"{args:\\#item of list}\")"); assert(t.nodes.nodeEq([ new Node(ln(0), "div", [ Attribute(ln(0), "({*ngFor})", [ AttributeContent.text("{args:"), AttributeContent.text("#"), AttributeContent.text("item of list}") ]) ]) ])); } @safe unittest { // translation import std.string : toUpper; static Location ln(int l) { return Location("string", l); } static string tr(string str) { return "("~toUpper(str)~")"; } assert(parseDiet!tr("foo& test").nodes.nodeEq([ new Node(ln(0), "foo", null, [ NodeContent.text("(TEST)", ln(0)) ], NodeAttribs.translated, "test") ])); assert(parseDiet!tr("foo& test #{x} it").nodes.nodeEq([ new Node(ln(0), "foo", null, [ NodeContent.text("(TEST ", ln(0)), NodeContent.interpolation("X", ln(0)), NodeContent.text(" IT)", ln(0)), ], NodeAttribs.translated, "test #{x} it") ])); assert(parseDiet!tr("|&x").nodes.nodeEq([ new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text("(X)", ln(0))], NodeAttribs.translated, "x") ])); assert(parseDiet!tr("foo&.\n\tbar\n\tbaz").nodes.nodeEq([ new Node(ln(0), "foo", null, [ NodeContent.text("(BAR)", ln(1)), NodeContent.text("\n(BAZ)", ln(2)) ], NodeAttribs.translated|NodeAttribs.textNode, "bar\nbaz") ])); } @safe unittest { // test expected errors void testFail(string diet, string msg) { try { parseDiet(diet); assert(false, "Expected exception was not thrown."); } catch (DietParserException ex) assert(ex.msg == msg, "Unexpected error message: "~ex.msg); } testFail("+test", "Expected node text separated by a space character or end of line, but got '+test'."); testFail(" test", "First node must not be indented."); testFail("test\n test\n\ttest", "Mismatched indentation style."); testFail("test\n\ttest\n\t\t\ttest", "Line is indented too deeply."); testFail("test#", "Expected identifier but got nothing."); testFail("test.()", "Expected identifier but got '('."); testFail("a #[b.]", "Multi-line text nodes are not permitted for inline-tags."); testFail("a #[b: c]", "Nested inline-tags not allowed."); testFail("a#foo#bar", "Only one \"id\" definition using '#' is allowed."); } @safe unittest { // includes Node[] parse(string diet) { auto files = [ InputFile("main.dt", diet), InputFile("inc.dt", "p") ]; return parseDiet(files).nodes; } void testFail(string diet, string msg) { try { parse(diet); assert(false, "Expected exception was not thrown"); } catch (DietParserException ex) { assert(ex.msg == msg, "Unexpected error message: "~ex.msg); } } assert(parse("include inc").nodeEq([ new Node(Location("inc.dt", 0), "p", null, null) ])); testFail("include main", "Dependency cycle detected for this module."); testFail("include inc2", "Missing include input file: inc2 for main.dt"); testFail("include #{p}", "Dynamic includes are not supported."); testFail("include inc\n\tp", "Only 'block' allowed as children of includes."); testFail("p\ninclude inc\n\tp", "Only 'block' allowed as children of includes."); } @safe unittest { // extensions Node[] parse(string diet) { auto files = [ InputFile("main.dt", diet), InputFile("root.dt", "html\n\tblock a\n\tblock b"), InputFile("intermediate.dt", "extends root\nblock a\n\tp"), InputFile("direct.dt", "block a") ]; return parseDiet(files).nodes; } void testFail(string diet, string msg) { try { parse(diet); assert(false, "Expected exception was not thrown"); } catch (DietParserException ex) { assert(ex.msg == msg, "Unexpected error message: "~ex.msg); } } assert(parse("extends root").nodeEq([ new Node(Location("root.dt", 0), "html", null, null) ])); assert(parse("extends root\nblock a\n\tdiv\nblock b\n\tpre").nodeEq([ new Node(Location("root.dt", 0), "html", null, [ NodeContent.tag(new Node(Location("main.dt", 2), "div", null, null)), NodeContent.tag(new Node(Location("main.dt", 4), "pre", null, null)) ]) ])); assert(parse("extends intermediate\nblock b\n\tpre").nodeEq([ new Node(Location("root.dt", 0), "html", null, [ NodeContent.tag(new Node(Location("intermediate.dt", 2), "p", null, null)), NodeContent.tag(new Node(Location("main.dt", 2), "pre", null, null)) ]) ])); assert(parse("extends intermediate\nblock a\n\tpre").nodeEq([ new Node(Location("root.dt", 0), "html", null, [ NodeContent.tag(new Node(Location("main.dt", 2), "pre", null, null)) ]) ])); assert(parse("extends intermediate\nappend a\n\tpre").nodeEq([ new Node(Location("root.dt", 0), "html", null, [ NodeContent.tag(new Node(Location("intermediate.dt", 2), "p", null, null)), NodeContent.tag(new Node(Location("main.dt", 2), "pre", null, null)) ]) ])); assert(parse("extends intermediate\nprepend a\n\tpre").nodeEq([ new Node(Location("root.dt", 0), "html", null, [ NodeContent.tag(new Node(Location("main.dt", 2), "pre", null, null)), NodeContent.tag(new Node(Location("intermediate.dt", 2), "p", null, null)) ]) ])); assert(parse("extends intermediate\nprepend a\n\tfoo\nappend a\n\tbar").nodeEq([ // issue #13 new Node(Location("root.dt", 0), "html", null, [ NodeContent.tag(new Node(Location("main.dt", 2), "foo", null, null)), NodeContent.tag(new Node(Location("intermediate.dt", 2), "p", null, null)), NodeContent.tag(new Node(Location("main.dt", 4), "bar", null, null)) ]) ])); assert(parse("extends intermediate\nprepend a\n\tfoo\nprepend a\n\tbar\nappend a\n\tbaz\nappend a\n\tbam").nodeEq([ new Node(Location("root.dt", 0), "html", null, [ NodeContent.tag(new Node(Location("main.dt", 2), "foo", null, null)), NodeContent.tag(new Node(Location("main.dt", 4), "bar", null, null)), NodeContent.tag(new Node(Location("intermediate.dt", 2), "p", null, null)), NodeContent.tag(new Node(Location("main.dt", 6), "baz", null, null)), NodeContent.tag(new Node(Location("main.dt", 8), "bam", null, null)) ]) ])); assert(parse("extends direct").nodeEq([])); assert(parse("extends direct\nblock a\n\tp").nodeEq([ new Node(Location("main.dt", 2), "p", null, null) ])); } @safe unittest { // include extensions Node[] parse(string diet) { auto files = [ InputFile("main.dt", diet), InputFile("root.dt", "p\n\tblock a"), ]; return parseDiet(files).nodes; } assert(parse("body\n\tinclude root\n\t\tblock a\n\t\t\tem").nodeEq([ new Node(Location("main.dt", 0), "body", null, [ NodeContent.tag(new Node(Location("root.dt", 0), "p", null, [ NodeContent.tag(new Node(Location("main.dt", 3), "em", null, null)) ])) ]) ])); assert(parse("body\n\tinclude root\n\t\tblock a\n\t\t\tem\n\tinclude root\n\t\tblock a\n\t\t\tstrong").nodeEq([ new Node(Location("main.dt", 0), "body", null, [ NodeContent.tag(new Node(Location("root.dt", 0), "p", null, [ NodeContent.tag(new Node(Location("main.dt", 3), "em", null, null)) ])), NodeContent.tag(new Node(Location("root.dt", 0), "p", null, [ NodeContent.tag(new Node(Location("main.dt", 6), "strong", null, null)) ])) ]) ])); } @safe unittest { // test CTFE-ability static const result = parseDiet("foo#id.cls(att=\"val\", att2=1+3, att3='test#{4}it')\n\tbar"); static assert(result.nodes.length == 1); } @safe unittest { // regression tests Location ln(int l) { return Location("string", l); } // last line contains only whitespace assert(parseDiet("test\n\t").nodes.nodeEq([ new Node(ln(0), "test") ])); } @safe unittest { // issue #14 - blocks in includes auto files = [ InputFile("main.dt", "extends layout\nblock nav\n\tbaz"), InputFile("layout.dt", "foo\ninclude inc"), InputFile("inc.dt", "bar\nblock nav"), ]; assert(parseDiet(files).nodes.nodeEq([ new Node(Location("layout.dt", 0), "foo", null, null), new Node(Location("inc.dt", 0), "bar", null, null), new Node(Location("main.dt", 2), "baz", null, null) ])); } @safe unittest { // issue #32 - numeric id/class Location ln(int l) { return Location("string", l); } assert(parseDiet("foo.01#02").nodes.nodeEq([ new Node(ln(0), "foo", [ Attribute(ln(0), "class", [AttributeContent.text("01")]), Attribute(ln(0), "id", [AttributeContent.text("02")]) ]) ])); } /** Dummy translation function that returns the input unmodified. */ string identity(string str, string context = null) nothrow @safe @nogc { return str; } private string parseIdent(in string str, ref size_t start, string breakChars, in Location loc) @safe { import std.array : back; /* The stack is used to keep track of opening and closing character pairs, so that when we hit a break char of breakChars we know if we can actually break parseIdent. */ char[] stack; size_t i = start; outer: while(i < str.length) { if(stack.length == 0) { foreach(char it; breakChars) { if(str[i] == it) { break outer; } } } if(stack.length && stack.back == str[i]) { stack = stack[0 .. $ - 1]; } else if(str[i] == '"') { stack ~= '"'; } else if(str[i] == '(') { stack ~= ')'; } else if(str[i] == '[') { stack ~= ']'; } else if(str[i] == '{') { stack ~= '}'; } ++i; } /* We could have consumed the complete string and still have elements on the stack or have ended non breakChars character. */ if(i < str.length && stack.length == 0) { foreach(char it; breakChars) { if(str[i] == it) { size_t startC = start; start = i; return str[startC .. i]; } } } enforcep(false, "Identifier was not ended by any of these characters: " ~ breakChars, loc); assert(false); } @safe unittest { // issue #75 string foo = "(failure"; Location loc; size_t pos = 1; import std.exception : assertThrown; assertThrown!(DietParserException)(parseIdent(foo, pos, ")", loc)); } private Node[] parseDietWithExtensions(FileInfo[] files, size_t file_index, ref BlockInfo[] blocks, size_t[] import_stack) @safe { import std.algorithm : all, any, canFind, countUntil, filter, find, map; import std.array : array; import std.path : stripExtension; import std.typecons : Nullable; auto floc = Location(files[file_index].name, 0); enforcep(!import_stack.canFind(file_index), "Dependency cycle detected for this module.", floc); Node[] nodes = files[file_index].nodes; if (!nodes.length) return null; if (nodes[0].name == "extends") { // extract base template name/index enforcep(nodes[0].isTextNode, "'extends' cannot contain children or interpolations.", nodes[0].loc); enforcep(nodes[0].attributes.length == 0, "'extends' cannot have attributes.", nodes[0].loc); string base_template = nodes[0].contents[0].value.ctstrip; auto base_idx = files.countUntil!(f => matchesName(f.name, base_template, files[file_index].name)); assert(base_idx >= 0, "Missing base template: "~base_template); // collect all blocks foreach (n; nodes[1 .. $]) { BlockInfo.Mode mode; switch (n.name) { default: enforcep(false, "Extension templates may only contain blocks definitions at the root level.", n.loc); break; case Node.SpecialName.comment, Node.SpecialName.hidden: continue; // also allow comments at the root level case "block": mode = BlockInfo.Mode.replace; break; case "prepend": mode = BlockInfo.Mode.prepend; break; case "append": mode = BlockInfo.Mode.append; break; } enforcep(n.contents.length > 0 && n.contents[0].kind == NodeContent.Kind.text, "'block' must have a name.", n.loc); auto name = n.contents[0].value.ctstrip; auto contents = n.contents[1 .. $].filter!(n => n.kind == NodeContent.Kind.node).map!(n => n.node).array; blocks ~= BlockInfo(name, mode, contents); } // save the original file contents for a possible later parsing as part of an // extension include directive (blocks are replaced in-place as part of the parsing // process) auto new_files = files.dup; new_files[base_idx].nodes = clone(new_files[base_idx].nodes); // parse base template return parseDietWithExtensions(new_files, base_idx, blocks, import_stack ~ file_index); } static string extractFilename(Node n) @safe { enforcep(n.contents.length >= 1 && n.contents[0].kind != NodeContent.Kind.node, "Missing block name.", n.loc); enforcep(n.contents[0].kind == NodeContent.Kind.text, "Dynamic includes are not supported.", n.loc); enforcep(n.contents.length == 1 || n.contents[1 .. $].all!(nc => nc.kind == NodeContent.Kind.node), "'"~n.name~"' must only contain a block name and child nodes.", n.loc); enforcep(n.attributes.length == 0, "'"~n.name~"' cannot have attributes.", n.loc); return n.contents[0].value.ctstrip; } Nullable!(Node[]) processNode(Node n) @safe { Nullable!(Node[]) ret; void insert(Node[] nodes) @safe { foreach (i, n; nodes) { auto np = processNode(n); if (!np.isNull()) { if (ret.isNull) ret = nodes[0 .. i]; ret.get ~= np.get; } else if (!ret.isNull) ret.get ~= n; } if (ret.isNull && nodes.length) ret = nodes; } if (n.name == "block") { auto name = extractFilename(n); auto blockdefs = blocks.filter!(b => b.name == name); foreach (b; blockdefs.save.filter!(b => b.mode == BlockInfo.Mode.prepend)) insert(b.contents); auto replblocks = blockdefs.save.find!(b => b.mode == BlockInfo.Mode.replace); if (!replblocks.empty) { insert(replblocks.front.contents); } else { insert(n.contents[1 .. $].map!((nc) { assert(nc.kind == NodeContent.Kind.node, "Block contains non-node child!?"); return nc.node; }).array); } foreach (b; blockdefs.save.filter!(b => b.mode == BlockInfo.Mode.append)) insert(b.contents); if (ret.isNull) ret = []; } else if (n.name == "include") { auto name = extractFilename(n); auto fidx = files.countUntil!(f => matchesName(f.name, name, n.loc.file)); enforcep(fidx >= 0, "Missing include input file: "~name~" for "~n.loc.file, n.loc); if (n.contents.length > 1) { auto dummy = new Node(n.loc, "extends"); dummy.addText(name, n.contents[0].loc); Node[] children = [dummy]; foreach (nc; n.contents[1 .. $]) { enforcep(nc.node !is null && nc.node.name == "block", "Only 'block' allowed as children of includes.", nc.loc); children ~= nc.node; } import std.path : extension; auto dummyfil = FileInfo("include"~extension(files[file_index].name), children); BlockInfo[] sub_blocks; insert(parseDietWithExtensions(files ~ dummyfil, files.length, sub_blocks, import_stack)); } else { insert(parseDietWithExtensions(files, fidx, blocks, import_stack ~ file_index)); } } else { n.contents = n.contents.mapJoin!((nc) { Nullable!(NodeContent[]) rn; if (nc.kind == NodeContent.Kind.node) { auto mod = processNode(nc.node); if (!mod.isNull()) rn = mod.get.map!(n => NodeContent.tag(n)).array; } assert(rn.isNull || rn.get.all!(n => n.node.name != "block")); return rn; }); } assert(ret.isNull || ret.get.all!(n => n.name != "block")); return ret; } nodes = nodes.mapJoin!(processNode); assert(nodes.all!(n => n.name != "block")); return nodes; } private struct BlockInfo { enum Mode { prepend, replace, append } string name; Mode mode = Mode.replace; Node[] contents; } private struct FileInfo { string name; Node[] nodes; } /** Parses a single Diet template file, without resolving includes and extensions. See_Also: `parseDiet` */ Node[] parseDietRaw(alias TR)(InputFile file) { import std.algorithm.iteration : map; import std.algorithm.comparison : among; import std.array : array; string indent_style; auto loc = Location(file.name, 0); int prevlevel = -1; string input = file.contents; Node[] ret; // nested stack of nodes // the first dimension is corresponds to indentation based nesting // the second dimension is for in-line nested nodes Node[][] stack; stack.length = 8; string previndent; // inherited by blank lines next_line: while (input.length) { Node pnode; if (prevlevel >= 0 && stack[prevlevel].length) pnode = stack[prevlevel][$-1]; // skip whitespace at the beginning of the line string indent = input.skipIndent(); // treat empty lines as if they had the indendation level of the last non-empty line if (input.empty || input[0].among('\n', '\r')) indent = previndent; else previndent = indent; enforcep(prevlevel >= 0 || indent.length == 0, "First node must not be indented.", loc); // determine the indentation style (tabs/spaces) from the first indented // line of the file if (indent.length && !indent_style.length) indent_style = indent; // determine nesting level bool is_text_line = pnode && (pnode.attribs & (NodeAttribs.textNode|NodeAttribs.rawTextNode)) != 0; int level = 0; if (indent_style.length) { while (indent.startsWith(indent_style)) { if (level > prevlevel) { enforcep(is_text_line, "Line is indented too deeply.", loc); break; } level++; indent = indent[indent_style.length .. $]; } } enforcep(is_text_line || indent.length == 0, "Mismatched indentation style.", loc); // read the whole line as text if the parent node is a pure text node // ("." suffix) or pure raw text node (e.g. comments) if (level > prevlevel && prevlevel >= 0) { if (pnode.attribs & NodeAttribs.textNode) { if (!pnode.contents.empty) { pnode.addText("\n", loc); if (pnode.attribs & NodeAttribs.translated) pnode.translationKey ~= "\n"; } if (indent.length) pnode.addText(indent, loc); parseTextLine!TR(input, pnode, loc); continue; } else if (pnode.attribs & NodeAttribs.rawTextNode) { if (!pnode.contents.empty) pnode.addText("\n", loc); if (indent.length) pnode.addText(indent, loc); auto tmploc = loc; pnode.addText(skipLine(input, loc), tmploc); continue; } } // skip empty lines if (input.empty) break; else if (input[0] == '\n') { loc.line++; input.popFront(); continue; } else if (input[0] == '\r') { loc.line++; input.popFront(); if (!input.empty && input[0] == '\n') input.popFront(); continue; } // parse the line and write it to the stack: if (stack.length < level+1) stack.length = level+1; if (input.startsWith("//")) { // comments auto n = new Node; n.loc = loc; if (input[2 .. $].startsWith("-")) { n.name = Node.SpecialName.hidden; input = input[3 .. $]; } else { n.name = Node.SpecialName.comment; input = input[2 .. $]; } n.attribs |= NodeAttribs.rawTextNode; auto tmploc = loc; n.addText(skipLine(input, loc), tmploc); stack[level] = [n]; } else if (input.startsWith('-')) { // D statements input = input[1 .. $]; auto n = new Node; n.loc = loc; n.name = Node.SpecialName.code; auto tmploc = loc; n.addText(skipLine(input, loc), tmploc); stack[level] = [n]; } else if (input.startsWith(':')) { // filters stack[level] = []; string chain; do { input = input[1 .. $]; size_t idx = 0; if (chain.length) chain ~= ' '; chain ~= skipIdent(input, idx, "-_", loc, false, true); input = input[idx .. $]; if (input.startsWith(' ')) input = input[1 .. $]; } while (input.startsWith(':')); Node chn = new Node; chn.loc = loc; chn.name = Node.SpecialName.filter; chn.attribs = NodeAttribs.textNode; chn.attributes = [Attribute(loc, "filterChain", [AttributeContent.text(chain)])]; stack[level] ~= chn; /*auto tmploc = loc; auto trailing = skipLine(input, loc); if (trailing.length) parseTextLine(input, chn, tmploc);*/ parseTextLine!TR(input, chn, loc); } else { // normal tag line bool has_nested; stack[level] = null; do stack[level] ~= parseTagLine!TR(input, loc, has_nested); while (has_nested); } // add it to its parent contents foreach (i; 1 .. stack[level].length) stack[level][i-1].contents ~= NodeContent.tag(stack[level][i]); if (level > 0) stack[level-1][$-1].contents ~= NodeContent.tag(stack[level][0]); else ret ~= stack[0][0]; // remember the nesting level for the next line prevlevel = level; } return ret; } private Node parseTagLine(alias TR)(ref string input, ref Location loc, out bool has_nested) { size_t idx = 0; auto ret = new Node; ret.loc = loc; if (input.startsWith("!!! ")) { // legacy doctype support input = input[4 .. $]; ret.name = "doctype"; parseTextLine!TR(input, ret, loc); return ret; } if (input.startsWith('<')) { // inline HTML/XML ret.name = Node.SpecialName.text; parseTextLine!TR(input, ret, loc); return ret; } if (input.startsWith('|')) { // text line input = input[1 .. $]; ret.name = Node.SpecialName.text; if (idx < input.length && input[idx] == '&') { ret.attribs |= NodeAttribs.translated; idx++; } } else { // normal tag if (parseTag(input, idx, ret, has_nested, loc)) return ret; } if (idx+1 < input.length && input[idx .. idx+2] == "!=") { enforcep(!(ret.attribs & NodeAttribs.translated), "Compile-time translation is not supported for (raw) assignments.", ret.loc); idx += 2; auto l = loc; ret.contents ~= NodeContent.rawInterpolation(ctstrip(skipLine(input, idx, loc)), l); input = input[idx .. $]; } else if (idx < input.length && input[idx] == '=') { enforcep(!(ret.attribs & NodeAttribs.translated), "Compile-time translation is not supported for assignments.", ret.loc); idx++; auto l = loc; ret.contents ~= NodeContent.interpolation(ctstrip(skipLine(input, idx, loc)), l); input = input[idx .. $]; } else { auto tmploc = loc; auto remainder = skipLine(input, idx, loc); input = input[idx .. $]; if (remainder.length && remainder[0] == ' ') { // parse the rest of the line as text contents (if any non-ws) remainder = remainder[1 .. $]; parseTextLine!TR(remainder, ret, tmploc); } else if (ret.name == Node.SpecialName.text) { // allow omitting the whitespace for "|" text nodes parseTextLine!TR(remainder, ret, tmploc); } else { import std.string : strip; enforcep(remainder.strip().length == 0, "Expected node text separated by a space character or end of line, but got '"~remainder~"'.", loc); } } return ret; } private bool parseTag(ref string input, ref size_t idx, ref Node dst, ref bool has_nested, ref Location loc) @safe { import std.ascii : isWhite; dst.name = skipIdent(input, idx, ":-_", loc, true); // a trailing ':' is not part of the tag name, but signals a nested node if (dst.name.endsWith(":")) { dst.name = dst.name[0 .. $-1]; idx--; } bool have_id = false; while (idx < input.length) { if (input[idx] == '#') { // node ID idx++; auto value = skipIdent(input, idx, "-_", loc); enforcep(value.length > 0, "Expected id.", loc); enforcep(!have_id, "Only one \"id\" definition using '#' is allowed.", loc); have_id = true; dst.attributes ~= Attribute.text("id", value, loc); } else if (input[idx] == '.') { // node classes if (idx+1 >= input.length || input[idx+1].isWhite) goto textBlock; idx++; auto value = skipIdent(input, idx, "-_", loc); enforcep(value.length > 0, "Expected class name identifier.", loc); dst.attributes ~= Attribute.text("class", value, loc); } else break; } // generic attributes if (idx < input.length && input[idx] == '(') parseAttributes(input, idx, dst, loc); // avoid whitespace inside of tag if (idx < input.length && input[idx] == '<') { idx++; dst.attribs |= NodeAttribs.fitInside; } // avoid whitespace outside of tag if (idx < input.length && input[idx] == '>') { idx++; dst.attribs |= NodeAttribs.fitOutside; } // avoid whitespace inside of tag (also allowed after >) if (!(dst.attribs & NodeAttribs.fitInside) && idx < input.length && input[idx] == '<') { idx++; dst.attribs |= NodeAttribs.fitInside; } // translate text contents if (idx < input.length && input[idx] == '&') { idx++; dst.attribs |= NodeAttribs.translated; } // treat nested lines as text if (idx < input.length && input[idx] == '.') { textBlock: dst.attribs |= NodeAttribs.textNode; idx++; skipLine(input, idx, loc); // ignore the rest of the line input = input[idx .. $]; return true; } // another nested tag on the same line if (idx < input.length && input[idx] == ':') { idx++; // skip trailing whitespace (but no line breaks) while (idx < input.length && (input[idx] == ' ' || input[idx] == '\t')) idx++; // see if we got anything left on the line if (idx < input.length) { if (input[idx] == '\n' || input[idx] == '\r') { // FIXME: should we rather error out here? skipLine(input, idx, loc); } else { // leaves the rest of the line to parse another tag has_nested = true; } } input = input[idx .. $]; return true; } return false; } /** Parses a single line of text (possibly containing interpolations and inline tags). If there a a newline at the end, it will be appended to the contents of the destination node. */ private void parseTextLine(alias TR, bool translate = true)(ref string input, ref Node dst, ref Location loc) { import std.algorithm.comparison : among; import std.path : baseName, stripExtension; size_t idx = 0; if (translate && dst.attribs & NodeAttribs.translated) { Location loccopy = loc; auto kln = skipLine(input, idx, loc); input = input[idx .. $]; dst.translationKey ~= kln; static if (is(typeof(TR(string.init, string.init)))) auto tln = TR(kln, loc.file.baseName.stripExtension); else auto tln = TR(kln); parseTextLineRaw(tln, dst, loccopy); return; } parseTextLineRaw(input, dst, loc); } private void parseTextLineRaw(ref string input, ref Node dst, ref Location loc) @safe { import std.algorithm.comparison : among; size_t sidx = 0, idx = 0; void flushText() @safe { if (idx > sidx) dst.addText(input[sidx .. idx], loc); } while (idx < input.length) { char cur = input[idx]; switch (cur) { default: idx++; break; case '\\': if (idx+1 < input.length && input[idx+1].among('#', '!')) { flushText(); sidx = idx+1; idx += 2; } else idx++; break; case '!', '#': if (idx+1 < input.length && input[idx+1] == '{') { flushText(); idx += 2; auto expr = skipUntilClosingBrace(input, idx, loc); idx++; if (cur == '#') dst.contents ~= NodeContent.interpolation(expr, loc); else dst.contents ~= NodeContent.rawInterpolation(expr, loc); sidx = idx; } else if (cur == '#' && idx+1 < input.length && input[idx+1] == '[') { flushText(); idx += 2; auto tag = skipUntilClosingBracket(input, idx, loc); idx++; bool has_nested; auto itag = parseTagLine!identity(tag, loc, has_nested); enforcep(!(itag.attribs & (NodeAttribs.textNode|NodeAttribs.rawTextNode)), "Multi-line text nodes are not permitted for inline-tags.", loc); enforcep(!(itag.attribs & NodeAttribs.translated), "Inline-tags cannot be translated individually.", loc); enforcep(!has_nested, "Nested inline-tags not allowed.", loc); dst.contents ~= NodeContent.tag(itag); sidx = idx; } else idx++; break; case '\r': flushText(); idx++; if (idx < input.length && input[idx] == '\n') idx++; input = input[idx .. $]; loc.line++; return; case '\n': flushText(); idx++; input = input[idx .. $]; loc.line++; return; } } flushText(); assert(idx == input.length); input = null; } private string skipLine(ref string input, ref size_t idx, ref Location loc) @safe { auto sidx = idx; while (idx < input.length) { char cur = input[idx]; switch (cur) { default: idx++; break; case '\r': auto ret = input[sidx .. idx]; idx++; if (idx < input.length && input[idx] == '\n') idx++; loc.line++; return ret; case '\n': auto ret = input[sidx .. idx]; idx++; loc.line++; return ret; } } return input[sidx .. $]; } private string skipLine(ref string input, ref Location loc) @safe { size_t idx = 0; auto ret = skipLine(input, idx, loc); input = input[idx .. $]; return ret; } private void parseAttributes(ref string input, ref size_t i, ref Node node, in Location loc) @safe { assert(i < input.length && input[i] == '('); i++; skipAnyWhitespace(input, i); while (i < input.length && input[i] != ')') { string name = parseIdent(input, i, ",)=", loc); string value; skipAnyWhitespace(input, i); if( i < input.length && input[i] == '=' ){ i++; skipAnyWhitespace(input, i); enforcep(i < input.length, "'=' must be followed by attribute string.", loc); value = skipExpression(input, i, loc, true); assert(i <= input.length); if (isStringLiteral(value) && value[0] == '\'') { auto tmp = dstringUnescape(value[1 .. $-1]); value = '"' ~ dstringEscape(tmp) ~ '"'; } } else value = "true"; enforcep(i < input.length, "Unterminated attribute section.", loc); enforcep(input[i] == ')' || input[i] == ',', "Unexpected text following attribute: '"~input[0..i]~"' ('"~input[i..$]~"')", loc); if (input[i] == ',') { i++; skipAnyWhitespace(input, i); } if (name == "class" && value == `""`) continue; if (isStringLiteral(value)) { AttributeContent[] content; parseAttributeText(value[1 .. $-1], content, loc); node.attributes ~= Attribute(loc, name, content); } else { node.attributes ~= Attribute.expr(name, value, loc); } } enforcep(i < input.length, "Missing closing clamp.", loc); i++; } private void parseAttributeText(string input, ref AttributeContent[] dst, in Location loc) @safe { size_t sidx = 0, idx = 0; void flushText() { if (idx > sidx) dst ~= AttributeContent.text(input[sidx .. idx]); } while (idx < input.length) { char cur = input[idx]; switch (cur) { default: idx++; break; case '\\': flushText(); dst ~= AttributeContent.text(dstringUnescape(sanitizeEscaping(input[idx .. idx+2]))); idx += 2; sidx = idx; break; case '!', '#': if (idx+1 < input.length && input[idx+1] == '{') { flushText(); idx += 2; auto expr = dstringUnescape(skipUntilClosingBrace(input, idx, loc)); idx++; if (cur == '#') dst ~= AttributeContent.interpolation(expr); else dst ~= AttributeContent.rawInterpolation(expr); sidx = idx; } else idx++; break; } } flushText(); input = input[idx .. $]; } private string skipUntilClosingBrace(in string s, ref size_t idx, in Location loc) @safe { import std.algorithm.comparison : among; int level = 0; auto start = idx; while( idx < s.length ){ if( s[idx] == '{' ) level++; else if( s[idx] == '}' ) level--; enforcep(!s[idx].among('\n', '\r'), "Missing '}' before end of line.", loc); if( level < 0 ) return s[start .. idx]; idx++; } enforcep(false, "Missing closing brace", loc); assert(false); } private string skipUntilClosingBracket(in string s, ref size_t idx, in Location loc) @safe { import std.algorithm.comparison : among; int level = 0; auto start = idx; while( idx < s.length ){ if( s[idx] == '[' ) level++; else if( s[idx] == ']' ) level--; enforcep(!s[idx].among('\n', '\r'), "Missing ']' before end of line.", loc); if( level < 0 ) return s[start .. idx]; idx++; } enforcep(false, "Missing closing bracket", loc); assert(false); } private string skipIdent(in string s, ref size_t idx, string additional_chars, in Location loc, bool accept_empty = false, bool require_alpha_start = false) @safe { import std.ascii : isAlpha; size_t start = idx; while (idx < s.length) { if (isAlpha(s[idx])) idx++; else if ((!require_alpha_start || start != idx) && s[idx] >= '0' && s[idx] <= '9') idx++; else { bool found = false; foreach (ch; additional_chars) if (s[idx] == ch) { found = true; idx++; break; } if (!found) { enforcep(accept_empty || start != idx, "Expected identifier but got '"~s[idx]~"'.", loc); return s[start .. idx]; } } } enforcep(start != idx, "Expected identifier but got nothing.", loc); return s[start .. idx]; } /// Skips all trailing spaces and tab characters of the input string. private string skipIndent(ref string input) @safe { size_t idx = 0; while (idx < input.length && isIndentChar(input[idx])) idx++; auto ret = input[0 .. idx]; input = input[idx .. $]; return ret; } private bool isIndentChar(dchar ch) @safe { return ch == ' ' || ch == '\t'; } private string skipAnyWhitespace(in string s, ref size_t idx) @safe { import std.ascii : isWhite; size_t start = idx; while (idx < s.length) { if (s[idx].isWhite) idx++; else break; } return s[start .. idx]; } private bool isStringLiteral(string str) @safe { size_t i = 0; // skip leading white space while (i < str.length && (str[i] == ' ' || str[i] == '\t')) i++; // no string literal inside if (i >= str.length) return false; char delimiter = str[i++]; if (delimiter != '"' && delimiter != '\'') return false; while (i < str.length && str[i] != delimiter) { if (str[i] == '\\') i++; i++; } // unterminated string literal if (i >= str.length) return false; i++; // skip delimiter // skip trailing white space while (i < str.length && (str[i] == ' ' || str[i] == '\t')) i++; // check if the string has ended with the closing delimiter return i == str.length; } @safe unittest { assert(isStringLiteral(`""`)); assert(isStringLiteral(`''`)); assert(isStringLiteral(`"hello"`)); assert(isStringLiteral(`'hello'`)); assert(isStringLiteral(` "hello" `)); assert(isStringLiteral(` 'hello' `)); assert(isStringLiteral(`"hel\"lo"`)); assert(isStringLiteral(`"hel'lo"`)); assert(isStringLiteral(`'hel\'lo'`)); assert(isStringLiteral(`'hel"lo'`)); assert(isStringLiteral(`'#{"address_"~item}'`)); assert(!isStringLiteral(`"hello\`)); assert(!isStringLiteral(`"hello\"`)); assert(!isStringLiteral(`"hello\"`)); assert(!isStringLiteral(`"hello'`)); assert(!isStringLiteral(`'hello"`)); assert(!isStringLiteral(`"hello""world"`)); assert(!isStringLiteral(`"hello" "world"`)); assert(!isStringLiteral(`"hello" world`)); assert(!isStringLiteral(`'hello''world'`)); assert(!isStringLiteral(`'hello' 'world'`)); assert(!isStringLiteral(`'hello' world`)); assert(!isStringLiteral(`"name" value="#{name}"`)); } private string skipExpression(in string s, ref size_t idx, in Location loc, bool multiline = false) @safe { string clamp_stack; size_t start = idx; outer: while (idx < s.length) { switch (s[idx]) { default: break; case '\n', '\r': enforcep(multiline, "Unexpected end of line.", loc); break; case ',': if (clamp_stack.length == 0) break outer; break; case '"', '\'': idx++; skipAttribString(s, idx, s[idx-1], loc); break; case '(': clamp_stack ~= ')'; break; case '[': clamp_stack ~= ']'; break; case '{': clamp_stack ~= '}'; break; case ')', ']', '}': if (s[idx] == ')' && clamp_stack.length == 0) break outer; enforcep(clamp_stack.length > 0 && clamp_stack[$-1] == s[idx], "Unexpected '"~s[idx]~"'", loc); clamp_stack.length--; break; } idx++; } enforcep(clamp_stack.length == 0, "Expected '"~clamp_stack[$-1]~"' before end of attribute expression.", loc); return ctstrip(s[start .. idx]); } private string skipAttribString(in string s, ref size_t idx, char delimiter, in Location loc) @safe { size_t start = idx; while( idx < s.length ){ if( s[idx] == '\\' ){ // pass escape character through - will be handled later by buildInterpolatedString idx++; enforcep(idx < s.length, "'\\' must be followed by something (escaped character)!", loc); } else if( s[idx] == delimiter ) break; idx++; } enforcep(idx < s.length, "Unterminated attribute string: "~s[start-1 .. $]~"||", loc); return s[start .. idx]; } private bool matchesName(string filename, string logical_name, string parent_name) @safe { import std.path : extension; if (filename == logical_name) return true; auto ext = extension(parent_name); if (filename.endsWith(ext) && filename[0 .. $-ext.length] == logical_name) return true; return false; } private T[] mapJoin(alias modify, T)(T[] arr) { T[] ret; size_t start = 0; foreach (i; 0 .. arr.length) { auto mod = modify(arr[i]); if (!mod.isNull()) { ret ~= arr[start .. i] ~ mod.get(); start = i + 1; } } if (start == 0) return arr; ret ~= arr[start .. $]; return ret; } diet-ng-1.8.1/source/diet/html.d0000644000175000017500000011311614230473643015745 0ustar nileshnilesh/** HTML output generator implementation. */ module diet.html; import diet.defs; import diet.dom; import diet.internal.html; import diet.internal.string; import diet.input; import diet.parser; import diet.traits; private template _dietFileData(string filename) { import diet.internal.string : stripUTF8BOM; private static immutable contents = stripUTF8BOM(import(filename)); } /** Compiles a Diet template file that is available as a string import. The resulting HTML is written to the output range given as a runtime parameter. Params: filename = Name of the main Diet template file. ALIASES = A list of variables to make available inside of the template, as well as traits structs annotated with the `@dietTraits` attribute. Traits: In addition to the default Diet traits, adding an enum field `htmlOutputStyle` of type `HTMLOutputStyle` to a traits struct can be used to control the style of the generated HTML. See_Also: `compileHTMLDietString`, `compileHTMLDietStrings` Example: --- import std.array : appender; auto text = appender!string; text.compileHTMLDietFile!("invitation-email.diet", name, address); sendMail(address, text.data); --- */ template compileHTMLDietFile(string filename, ALIASES...) { alias compileHTMLDietFile = compileHTMLDietFileString!(filename, _dietFileData!filename.contents, ALIASES); } version(DietUseLive) { // out here, because the FileInfo struct isn't different based on the TRAITS. private struct FileInfo { import std.datetime : SysTime; SysTime modTime; string[] dependencies; string[] htmlstrings; } private string[] _getHTMLStrings(TRAITS...)(string filename, string expectedCode) @safe { import std.range : chain; import std.file; import std.array; import std.algorithm; import std.string : lineSplitter; static FileInfo[string] cache; // one per set of TRAITS. // assume files live in views/filename if(auto fi = filename in cache) { // have to check all the files, not just the main one bool newer = false; foreach(dep; fi.dependencies) { auto curMod = chain("views/", dep).timeLastModified; if(curMod > fi.modTime) { newer = true; break; } } // already checked, return the strings if(!newer) return fi.htmlstrings; } auto inputs = rtGetInputs(filename, "views/"); // need to process the file again auto doc = applyTraits!TRAITS(parseDiet!(translate!TRAITS)(inputs)); auto code = getHTMLLiveMixin(doc); // remove all the "#line" directives and compare the code. If it doesn't // match, then the code changes might affect the output, and a recompile is // necessary. if(!code.lineSplitter.filter!(l => !l.startsWith("#line")).equal(expectedCode.lineSplitter.filter!(l => !l.startsWith("#line")))) { throw new DietParserException("Recompile necessary! view file " ~ filename ~ " or dependency has changed its code"); } auto curMod = chain("views/", inputs[0].name).timeLastModified; foreach(x; inputs[1 .. $]) { // find latest time modified curMod = max(curMod, chain("views/", x.name).timeLastModified); } auto newFI = FileInfo(curMod, inputs.map!(fi => fi.name).array, getHTMLRawTextOnly(doc, dietOutputRangeName, getHTMLOutputStyle!TRAITS).splitter('\0').array); cache[filename] = newFI; return newFI.htmlstrings; } } // provide a place to cache compilation of a file. No reason to rebuild every // time a file is used. private template realCompileHTMLDietFileString(string filename, alias contents, TRAITS...) { import std.conv : to; private static immutable _diet_files = collectFiles!(filename, contents); version (DietUseCache) { enum _diet_use_cache = true; ulong computeTemplateHash() { ulong ret = 0; void hash(string s) { foreach (char c; s) { ret *= 9198984547192449281; ret += c * 7576889555963512219; } } foreach (ref f; _diet_files) { hash(f.name); hash(f.contents); } return ret; } enum _diet_hash = computeTemplateHash(); enum _diet_cache_file_name = filename~"_cached_"~_diet_hash.to!string~".d"; } else { enum _diet_use_cache = false; enum _diet_cache_file_name = "***INVALID***"; // not used anyway } static if (_diet_use_cache && is(typeof(import(_diet_cache_file_name)))) { pragma(msg, "Using cached Diet HTML template "~filename~"..."); enum _dietParser = import(_diet_cache_file_name); } else { pragma(msg, "Compiling Diet HTML template "~filename~"..."); private Document _diet_nodes() { return applyTraits!TRAITS(parseDiet!(translate!TRAITS)(_diet_files)); } version(DietUseLive) { enum _dietParser = getHTMLLiveMixin(_diet_nodes(), dietOutputRangeName); } else { enum _dietParser = getHTMLMixin(_diet_nodes(), dietOutputRangeName, getHTMLOutputStyle!TRAITS); } static if (_diet_use_cache) { shared static this() { import std.file : exists, write; if (!exists("views/"~_diet_cache_file_name)) write("views/"~_diet_cache_file_name, _dietParser); } } } } /** Compiles a Diet template given as a string, with support for includes and extensions. This function behaves the same as `compileHTMLDietFile`, except that the contents of the file are The final HTML will be written to the given `_diet_output` output range. Params: filename = The name to associate with `contents` contents = The contents of the Diet template ALIASES = A list of variables to make available inside of the template, as well as traits structs annotated with the `@dietTraits` attribute. See_Also: `compileHTMLDietFile`, `compileHTMLDietString`, `compileHTMLDietStrings` */ template compileHTMLDietFileString(string filename, alias contents, ALIASES...) { // This import should be REMOVED for 2.0.0, as it was unintentionally // exposed for use inside the mixin. See issue #81 import std.conv : to; alias TRAITS = DietTraits!ALIASES; alias _dietParser = realCompileHTMLDietFileString!(filename, contents, TRAITS)._dietParser; version(DietUseLive) { // uses the correct range name and removes 'dst' from the scope private void exec(R)(ref R _diet_output, string[] _diet_html_strings) { mixin(localAliasesMixin!(0, ALIASES)); //pragma(msg, _dietParser); mixin(_dietParser); } /** * See `.compileHTMLDietFileString` * * Params: * dst = The output range to write the generated HTML to. */ void compileHTMLDietFileString(R)(ref R dst) { // first, load the data exec(dst, _getHTMLStrings!TRAITS(filename, _dietParser)); } } else { // uses the correct range name and removes 'dst' from the scope private void exec(R)(ref R _diet_output) { mixin(localAliasesMixin!(0, ALIASES)); //pragma(msg, _dietParser); mixin(_dietParser); } /** * See `.compileHTMLDietFileString` * * Params: * dst = The output range to write the generated HTML to. */ void compileHTMLDietFileString(R)(ref R dst) { exec(dst); } } } /** Compiles a Diet template given as a string. The final HTML will be written to the given `_diet_output` output range. Params: contents = The contents of the Diet template ALIASES = A list of variables to make available inside of the template, as well as traits structs annotated with the `@dietTraits` attribute. dst = The output range to write the generated HTML to. See_Also: `compileHTMLDietFileString`, `compileHTMLDietStrings` */ template compileHTMLDietString(string contents, ALIASES...) { void compileHTMLDietString(R)(ref R dst) { compileHTMLDietStrings!(Group!(contents, "diet-string"), ALIASES)(dst); } } /** Compiles a set of Diet template files. The final HTML will be written to the given `_diet_output` output range. Params: FILES_GROUP = A `diet.input.Group` containing an alternating list of file names and file contents. ALIASES = A list of variables to make available inside of the template, as well as traits structs annotated with the `@dietTraits` attribute. dst = The output range to write the generated HTML to. See_Also: `compileHTMLDietString`, `compileHTMLDietStrings` */ template compileHTMLDietStrings(alias FILES_GROUP, ALIASES...) { alias TRAITS = DietTraits!ALIASES; private static Document _diet_nodes() { return applyTraits!TRAITS(parseDiet!(translate!TRAITS)(filesFromGroup!FILES_GROUP)); } // uses the correct range name and removes 'dst' from the scope private void exec(R)(ref R _diet_output) { mixin(localAliasesMixin!(0, ALIASES)); //pragma(msg, getHTMLMixin(_diet_nodes())); mixin(getHTMLMixin(_diet_nodes(), dietOutputRangeName, getHTMLOutputStyle!TRAITS)); } void compileHTMLDietStrings(R)(ref R dst) { exec(dst); } } // encapsulate this externally for maintenance and for testing. private enum _diet_imports = "import diet.internal.html : htmlEscape, htmlAttribEscape, filterHTMLAttribEscape;\n" ~ "import std.format : formattedWrite;\n" ~ "import std.range : put;\n"; /** Returns a mixin string that generates HTML for the given DOM tree. Params: doc = The root nodes of the DOM tree. range_name = Optional custom name to use for the output range, defaults to `_diet_output`. style = Output style to use. Returns: A string of D statements suitable to be mixed in inside of a function. */ string getHTMLMixin(in Document doc, string range_name = dietOutputRangeName, HTMLOutputStyle style = HTMLOutputStyle.compact) { CTX ctx; ctx.pretty = style == HTMLOutputStyle.pretty; ctx.rangeName = range_name; string ret = _diet_imports; foreach (i, n; doc.nodes) ret ~= ctx.getHTMLMixin(n, false); ret ~= ctx.flushRawText(); return ret; } /** This is like getHTMLMixin, but returns only the NON-code portions of the diet template. The usage is for the DietLiveMode, which can update the HTML portions of the diet template at runtime without requiring a recompile. Params: doc = The root nodes of the DOM tree. range_name = Optional custom name to use for the output range, defaults to `_diet_output`. style = Output style to use. Returns: The return value is a concatenated string with each string of raw HTML text separated by a null character. To extract the strings to send into the live renderer, split the string based on a null character. */ string getHTMLRawTextOnly(in Document doc, string range_name = dietOutputRangeName, HTMLOutputStyle style = HTMLOutputStyle.compact) @safe { CTX ctx; ctx.pretty = style == HTMLOutputStyle.pretty; ctx.mode = CTX.OutputMode.rawTextOnly; ctx.rangeName = range_name; // definitely don't want the top imports here string ret; foreach(i, n; doc.nodes) ret ~= ctx.getHTMLMixin(n, false); ret ~= ctx.flushRawText(); return ret; } /** This returns a "live" version of the mixin. The live version generates the code skeleton and then accepts a list of HTML strings that go between the code to output. This way, you can read the diet template at runtime, and if any non-code changes are made, you can avoid recompilation. */ string getHTMLLiveMixin(in Document doc, string range_name = dietOutputRangeName, string htmlPiecesMapName = "_diet_html_strings") @safe { CTX ctx; ctx.mode = CTX.OutputMode.live; ctx.rangeName = range_name; ctx.piecesMapName = htmlPiecesMapName; string ret = _diet_imports; foreach(i, n; doc.nodes) ret ~= ctx.getHTMLMixin(n, false); // output a final html in case there were any items at the end ret ~= ctx.statement(Location("_livediet", 0), ""); return ret; } unittest { import diet.parser; void test(string src)(string expected) { import std.array : appender, array; import std.algorithm : splitter; 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); } // test live mode. { // generate the strings auto _diet_output = appender!string(); auto _diet_html_strings = getHTMLRawTextOnly(n).splitter('\0').array; mixin(getHTMLLiveMixin(n)); assert(_diet_output.data == expected, _diet_output.data); } } test!"doctype html\nfoo(test=true)"(""); test!"doctype html X\nfoo(test=true)"(""); test!"doctype X\nfoo(test=true)"(""); test!"foo(test=2+3)"(""); test!"foo(test='#{2+3}')"(""); test!"foo #{2+3}"("5"); test!"foo= 2+3"("5"); test!"- int x = 3;\nfoo=x"("3"); test!"- foreach (i; 0 .. 2)\n\tfoo"(""); test!"div(*ngFor=\"\\#item of list\")"( "
" ); test!".foo"("
"); 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. To use this, put an enum field named `htmlOutputStyle` into a diet traits struct and pass that to the render function. The default output style is `compact`. */ enum HTMLOutputStyle { compact, /// Outputs no extraneous whitespace (including line breaks) around HTML tags pretty, /// Inserts line breaks and indents lines according to their nesting level in the HTML structure } /// unittest { @dietTraits struct Traits { enum htmlOutputStyle = HTMLOutputStyle.pretty; } import std.array : appender; auto dst = appender!string(); dst.compileHTMLDietString!("html\n\tbody\n\t\tp Hello", Traits); import std.conv : to; assert(dst.data == "\n\t\n\t\t

Hello

\n\t\n", [dst.data].to!string); } private @property template getHTMLOutputStyle(TRAITS...) { static if (TRAITS.length) { static if (is(typeof(TRAITS[0].htmlOutputStyle))) enum getHTMLOutputStyle = TRAITS[0].htmlOutputStyle; else enum getHTMLOutputStyle = getHTMLOutputStyle!(TRAITS[1 .. $]); } else enum getHTMLOutputStyle = HTMLOutputStyle.compact; } private string getHTMLMixin(ref CTX ctx, in Node node, bool in_pre) @safe { switch (node.name) { default: return ctx.getElementMixin(node, in_pre); 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.hidden: return null; case Node.SpecialName.text: string ret; foreach (i, c; node.contents) ret ~= ctx.getNodeContentsMixin(c, in_pre); if (in_pre) ctx.plainNewLine(); else ctx.prettyNewLine(); return ret; } } private string getElementMixin(ref CTX ctx, in Node node, bool in_pre) @safe { import std.algorithm : countUntil; if (node.name == "pre") in_pre = true; bool need_newline = ctx.needPrettyNewline(node.contents); bool is_singular_tag; // determine if we need a closing tag or have a singular tag if (ctx.isHTML) { switch (node.name) { default: break; case "area", "base", "basefont", "br", "col", "embed", "frame", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr": is_singular_tag = true; need_newline = true; break; } } else if (!node.hasNonWhitespaceContent) is_singular_tag = true; // write tag name string tagname = node.name.length ? node.name : "div"; string ret; if (node.attribs & NodeAttribs.fitOutside || in_pre) ctx.inhibitNewLine(); else if (need_newline) ctx.prettyNewLine(); ret ~= ctx.rawText(node.loc, "<"~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 || (ca.isText && !ca.expectText.length)) continue; att.addText(" "); att.addContents(ca.contents); } } bool 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") { if (ctx.isHTML5) ret ~= ctx.rawText(node.loc, " "~att.name); else ret ~= ctx.rawText(node.loc, " "~att.name~"=\""~att.name~"\""); 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.rawText(node.loc, " "~att.name); else 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.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.statementCont(node.loc, "}} else {"); } ret ~= ctx.rawText(node.loc, " "~att.name ~ "=\""); foreach (i, v; att.contents) { final switch (v.kind) with (AttributeContent.Kind) { case text: ret ~= ctx.rawText(node.loc, htmlAttribEscape(v.value)); break; case interpolation, rawInterpolation: ret ~= ctx.statement(node.loc, q{%s.htmlAttribEscape(%s);}, ctx.rangeName, v.value); break; } } ret ~= ctx.rawText(node.loc, "\""); if (is_expr) ret ~= ctx.statement(node.loc, "}"); } // determine if we need a closing tag or have a singular tag if (is_singular_tag) { enforcep(!node.hasNonWhitespaceContent, "Singular HTML element '"~node.name~"' may not have contents.", node.loc); ret ~= ctx.rawText(node.loc, "/>"); if (need_newline && !(node.attribs & NodeAttribs.fitOutside)) ctx.prettyNewLine(); return ret; } ret ~= ctx.rawText(node.loc, ">"); // write contents if (need_newline) { ctx.depth++; if (!(node.attribs & NodeAttribs.fitInside) && !in_pre) ctx.prettyNewLine(); } foreach (i, c; node.contents) ret ~= ctx.getNodeContentsMixin(c, in_pre); if (need_newline && !in_pre) { ctx.depth--; if (!(node.attribs & NodeAttribs.fitInside) && !in_pre) ctx.prettyNewLine(); } else ctx.inhibitNewLine(); // write end tag ret ~= ctx.rawText(node.loc, ""); if ((node.attribs & NodeAttribs.fitOutside) || in_pre) ctx.inhibitNewLine(); else if (need_newline) ctx.prettyNewLine(); return ret; } private string getNodeContentsMixin(ref CTX ctx, in NodeContent c, bool in_pre) @safe { final switch (c.kind) with (NodeContent.Kind) { case node: return getHTMLMixin(ctx, c.node, in_pre); case text: return ctx.rawText(c.loc, c.value); case interpolation: return ctx.textStatement(c.loc, q{%s.htmlEscape(%s);}, ctx.rangeName, c.value); case rawInterpolation: return ctx.textStatement(c.loc, q{() @trusted { return (&%s); } ().formattedWrite("%%s", %s);}, ctx.rangeName, c.value); } } private string getDoctypeMixin(ref CTX ctx, in Node node) @safe { import std.algorithm.searching : startsWith; import diet.internal.string; if (node.name == "!!!") ctx.statement(node.loc, q{pragma(msg, "Use of '!!!' is deprecated. Use 'doctype' instead.");}); enforcep(node.contents.length == 1 && node.contents[0].kind == NodeContent.Kind.text, "Only doctype specifiers allowed as content for doctype nodes.", node.loc); auto args = ctstrip(node.contents[0].value); ctx.isHTML5 = false; string doctype_str = "!DOCTYPE html"; switch (args) { case "5": case "": case "html": ctx.isHTML5 = true; break; case "xml": doctype_str = `?xml version="1.0" encoding="utf-8" ?`; ctx.isHTML = false; break; case "transitional": doctype_str = `!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" ` ~ `"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"`; break; case "strict": doctype_str = `!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ` ~ `"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"`; break; case "frameset": doctype_str = `!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" ` ~ `"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd"`; break; case "1.1": doctype_str = `!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" ` ~ `"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"`; break; case "basic": doctype_str = `!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" ` ~ `"http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd"`; break; case "mobile": doctype_str = `!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" ` ~ `"http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd"`; break; default: doctype_str = "!DOCTYPE " ~ args; ctx.isHTML = args.startsWith("html "); break; } return ctx.rawText(node.loc, "<"~doctype_str~">"); } private string getCodeMixin(ref CTX ctx, const ref Node node, bool in_pre) @safe { 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; bool have_contents = node.contents.length > 1; foreach (i, c; node.contents) { if (i == 0 && c.kind == NodeContent.Kind.text) { if(have_contents) ret ~= ctx.statement(node.loc, "%s\n{", c.value); else ret ~= ctx.statement(node.loc, "%s", c.value); } else { assert(c.kind == NodeContent.Kind.node); ret ~= ctx.getHTMLMixin(c.node, in_pre); } } if(have_contents) ret ~= ctx.statement(node.loc, "}"); return ret; } private string getCommentMixin(ref CTX ctx, const ref Node node) @safe { string ret = ctx.rawText(node.loc, ""); return ret; } private struct CTX { @safe: enum NewlineState { none, plain, pretty, inhibit } bool isHTML5, isHTML = true; bool pretty; enum OutputMode { normal, live, rawTextOnly } OutputMode mode; int depth = 0; string rangeName; string piecesMapName; char[] piecesMapOutputStr; size_t currentStatement; bool inRawText = false; NewlineState newlineState = NewlineState.none; bool anyText; int suppressLive; // trying to cut down on compile time memory, this should help by not formatting very similar lines. pure @safe const(char)[] getHTMLPiece() { if(!piecesMapOutputStr.length) { piecesMapOutputStr = "put(" ~ rangeName ~ ", " ~ piecesMapName ~ "[0x00000000]);\n".dup; } // The last characters of the string are "[0x00000000]);\n". We can // replace the 0s with hex characters representing the bytes of the // index. Since we are always increasing the index, there's no need to // keep replacing 0s once the index is out of data size_t idx = piecesMapOutputStr.length - 5; size_t curIdx = currentStatement; while(curIdx) { immutable n = curIdx & 0x0f; if(n > 9) piecesMapOutputStr[idx] = 'a' + n - 10; else piecesMapOutputStr[idx] = '0' + n; --idx; curIdx >>= 4; } 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; import std.algorithm : splitter; string ret = flushRawText(); // Notes on live mode here. This is about to output a statement in D // code from the diet template. In live mode, this means we need to // output any HTML text before outputting the D line. Because we don't // know if someone might add HTML output where there currently isn't // any, we always output another string from the table even though it // might be empty. // // There are 2 cases where the code avoids doing this. The first is // between an `if` an `else` statement. D does not allow this in the // grammar (and it wouldn't make sense anyway). It is technically // possible to add HTML in the diet file between these two, but it will // not compile anyway. // // The second case is after a return statement. This one is tricky // because we need to suppress it on the closing brace. In practice, // the return statement will not have an HTML or any other statement // printout (or it will fail to compile), so a flag is stored that // indicates the next statement should suppress "possible" HTML output. // // At this time, the code just does a simple match to the keywords // `return` or `else` as the first word of the line. This should be // good enough, but may not be sufficient in all cases. auto nextLine = (fmt~"\n").format(args); auto firstNonSpace = nextLine.splitter; immutable isReturn = !firstNonSpace.empty && (firstNonSpace.front == "return" || firstNonSpace.front == "return;"); immutable isElse = !firstNonSpace.empty && firstNonSpace.front == "else"; with(OutputMode) final switch(mode) { case rawTextOnly: // each statement is represented by a null character as a placeholder. if(!isElse && !suppressLive) ret ~= '\0'; break; case live: // output all non-statement data until this point. if(!isElse && !suppressLive) { ret ~= getHTMLPiece(); } // fall through goto case normal; case normal: ret ~= ("#line %s \"%s\"\n").format(loc.line+1, loc.file); ret ~= nextLine; break; } if(!isElse) { if(suppressLive) --suppressLive; else ++currentStatement; } if(isReturn) { // need to skip next HTML output suppressLive = 1; } return ret; } pure string textStatement(ARGS...)(Location loc, string fmt, ARGS args) { string ret; if (newlineState != NewlineState.none) ret ~= rawText(loc, null); ret ~= statement(loc, fmt, args); return ret; } pure string rawText(ARGS...)(Location loc, string text) { string ret; if (!this.inRawText) { with(OutputMode) final switch(mode) { case rawTextOnly: case live: // do nothing break; case normal: ret = "put(" ~ this.rangeName ~ ", \""; break; } this.inRawText = true; } ret ~= outputPendingNewline(); with(OutputMode) final switch(mode) { case live: // do nothing break; case normal: ret ~= dstringEscape(text); break; case rawTextOnly: // this is the raw string being output to the browser, indexed in // an array. Since it's not being mixed in, we do not need to // escape. ret ~= text; break; } anyText = true; return ret; } pure string flushRawText() { if (this.inRawText) { this.inRawText = false; if(mode == OutputMode.normal) return "\");\n"; } return null; } void plainNewLine() { if (newlineState != NewlineState.inhibit) newlineState = NewlineState.plain; } void prettyNewLine() { if (newlineState != NewlineState.inhibit) newlineState = NewlineState.pretty; } void inhibitNewLine() { newlineState = NewlineState.inhibit; } bool needPrettyNewline(in NodeContent[] contents) { import std.algorithm.searching : any; return pretty && contents.any!(c => c.kind == NodeContent.Kind.node); } private pure string outputPendingNewline() { auto st = newlineState; newlineState = NewlineState.none; if(mode == OutputMode.live) return null; final switch (st) { case NewlineState.none: return null; case NewlineState.inhibit:return null; case NewlineState.plain: return "\n"; case NewlineState.pretty: import std.array : replicate; return anyText ? "\n"~"\t".replicate(depth) : null; } } } unittest { static string compile(string diet, ALIASES...)() { import std.array : appender; import std.string : strip; auto dst = appender!string; compileHTMLDietString!(diet, ALIASES)(dst); return strip(cast(string)(dst.data)); } assert(compile!(`!!! 5`) == ``, `_`~compile!(`!!! 5`)~`_`); assert(compile!(`!!! html`) == ``); assert(compile!(`doctype html`) == ``); assert(compile!(`doctype xml`) == ``); assert(compile!(`p= 5`) == `

5

`); assert(compile!(`script= 5`) == ``); assert(compile!(`style= 5`) == ``); //assert(compile!(`include #{"p Hello"}`) == "

Hello

"); assert(compile!(`

Hello

`) == "

Hello

"); assert(compile!(`// I show up`) == ""); assert(compile!(`//-I don't show up`) == ""); assert(compile!(`//- I don't show up`) == ""); // issue 372 assert(compile!(`div(class="")`) == `
`); assert(compile!(`div.foo(class="")`) == `
`); assert(compile!(`div.foo(class="bar")`) == `
`); assert(compile!(`div(class="foo")`) == `
`); assert(compile!(`div#foo(class='')`) == `
`); // issue 19 assert(compile!(`input(checked=false)`) == ``); assert(compile!(`input(checked=true)`) == ``); assert(compile!(`input(checked=(true && false))`) == ``); assert(compile!(`input(checked=(true || false))`) == ``); assert(compile!(q{- import std.algorithm.searching : any; input(checked=([false].any))}) == ``); assert(compile!(q{- import std.algorithm.searching : any; input(checked=([true].any))}) == ``); assert(compile!(q{- bool foo() { return false; } input(checked=foo)}) == ``); assert(compile!(q{- bool foo() { return true; } input(checked=foo)}) == ``); // issue 520 assert(compile!("- auto cond = true;\ndiv(someattr=cond ? \"foo\" : null)") == "
"); assert(compile!("- auto cond = false;\ndiv(someattr=cond ? \"foo\" : null)") == "
"); assert(compile!("- auto cond = false;\ndiv(someattr=cond ? true : false)") == "
"); assert(compile!("- auto cond = true;\ndiv(someattr=cond ? true : false)") == "
"); assert(compile!("doctype html\n- auto cond = true;\ndiv(someattr=cond ? true : false)") == "
"); assert(compile!("doctype html\n- auto cond = false;\ndiv(someattr=cond ? true : false)") == "
"); // issue 510 assert(compile!("pre.test\n\tfoo") == "
"); assert(compile!("pre.test.\n\tfoo") == "
foo
"); assert(compile!("pre.test. foo") == "
");
	assert(compile!("pre().\n\tfoo") == "
foo
"); assert(compile!("pre#foo.test(data-img=\"sth\",class=\"meh\"). something\n\tmeh") == "
meh
"); assert(compile!("input(autofocus)").length); assert(compile!("- auto s = \"\";\ninput(type=\"text\",value=\"&\\\"#{s}\")") == ``); assert(compile!("- auto param = \"t=1&u=1\";\na(href=\"/?#{param}&v=1\") foo") == `foo`); // issue #1021 assert(compile!("html( lang=\"en\" )") == ""); // issue #1033 assert(compile!("input(placeholder=')')") == ""); assert(compile!("input(placeholder='(')") == ""); } unittest { // blocks and extensions static string compilePair(string extension, string base, ALIASES...)() { import std.array : appender; import std.string : strip; auto dst = appender!string; compileHTMLDietStrings!(Group!(extension, "extension.dt", base, "base.dt"), ALIASES)(dst); return strip(dst.data); } assert(compilePair!("extends base\nblock test\n\tp Hello", "body\n\tblock test") == "

Hello

"); assert(compilePair!("extends base\nblock test\n\tp Hello", "body\n\tblock test\n\t\tp Default") == "

Hello

"); assert(compilePair!("extends base", "body\n\tblock test\n\t\tp Default") == "

Default

"); assert(compilePair!("extends base\nprepend test\n\tp Hello", "body\n\tblock test\n\t\tp Default") == "

Hello

Default

"); } /*@nogc*/ @safe unittest { // NOTE: formattedWrite is not @nogc static struct R { @nogc @safe nothrow: void put(in char[]) {} void put(char) {} void put(dchar) {} } R r; r.compileHTMLDietString!( `doctype html html - foreach (i; 0 .. 10) title= i title t #{12} !{13} `); } unittest { // issue 4 - nested text in code static string compile(string diet, ALIASES...)() { import std.array : appender; import std.string : strip; auto dst = appender!string; compileHTMLDietString!(diet, ALIASES)(dst); return strip(cast(string)(dst.data)); } assert(compile!"- if (true)\n\t| int bar;" == "int bar;"); } unittest { // class instance variables import std.array : appender; import std.string : strip; static class C { int x = 42; string test() { auto dst = appender!string; dst.compileHTMLDietString!("| #{x}", x); return dst.data; } } auto c = new C; assert(c.test().strip == "42"); } unittest { // raw interpolation for non-copyable range struct R { @disable this(this); void put(dchar) {} void put(in char[]) {} } R r; r.compileHTMLDietString!("a !{2}"); } unittest { assert(utCompile!(".foo(class=true?\"bar\":\"baz\")") == "
"); } version (unittest) { private string utCompile(string diet, ALIASES...)() { import std.array : appender; import std.string : strip; auto dst = appender!string; compileHTMLDietString!(diet, ALIASES)(dst); return strip(cast(string)(dst.data)); } } unittest { // blank lines in text blocks assert(utCompile!("pre.\n\tfoo\n\n\tbar") == "
foo\n\nbar
"); } unittest { // singular tags should be each on their own line enum src = "p foo\nlink\nlink"; enum dst = "

foo

\n\n"; @dietTraits struct T { enum HTMLOutputStyle htmlOutputStyle = HTMLOutputStyle.pretty; } assert(utCompile!(src, T) == dst); } unittest { // ignore whitespace content for singular tags assert(utCompile!("link ") == ""); assert(utCompile!("link \n\t ") == ""); } unittest { @dietTraits struct T { enum HTMLOutputStyle htmlOutputStyle = HTMLOutputStyle.pretty; } import std.conv : to; // no extraneous newlines before text lines assert(utCompile!("foo\n\tbar text1\n\t| text2", T) == "\n\ttext1text2\n"); assert(utCompile!("foo\n\tbar: baz\n\t| text2", T) == "\n\t\n\t\t\n\t\n\ttext2\n"); // fit inside/outside + pretty printing - issue #27 assert(utCompile!("| foo\na<> bar\n| baz", T) == "foobarbaz"); assert(utCompile!("foo\n\ta< bar", T) == "\n\tbar\n"); assert(utCompile!("foo\n\ta> bar", T) == "bar"); assert(utCompile!("a\nfoo<\n\ta bar\nb", T) == "\nbar\n"); assert(utCompile!("a\nfoo>\n\ta bar\nb", T) == "\n\tbar\n"); // hard newlines in pre blocks assert(utCompile!("pre\n\t| foo\n\t| bar", T) == "
foo\nbar
"); assert(utCompile!("pre\n\tcode\n\t\t| foo\n\t\t| bar", T) == "
foo\nbar
"); // always hard breaks for text blocks assert(utCompile!("pre.\n\tfoo\n\tbar", T) == "
foo\nbar
"); assert(utCompile!("foo.\n\tfoo\n\tbar", T) == "foo\nbar"); } unittest { // issue #45 - no singular tags for XML assert(!__traits(compiles, utCompile!("doctype html\nlink foo"))); assert(!__traits(compiles, utCompile!("doctype html FOO\nlink foo"))); assert(utCompile!("doctype xml\nlink foo") == `foo`); assert(utCompile!("doctype foo\nlink foo") == `foo`); } unittest { // output empty tags as singular for XML output assert(utCompile!("doctype html\nfoo") == ``); assert(utCompile!("doctype xml\nfoo") == ``); } diet-ng-1.8.1/makepot/0000755000175000017500000000000014230473643014044 5ustar nileshnileshdiet-ng-1.8.1/makepot/source/0000755000175000017500000000000014230473643015344 5ustar nileshnileshdiet-ng-1.8.1/makepot/source/app.d0000644000175000017500000000313414230473643016272 0ustar nileshnileshimport diet.input; import diet.internal.string : dstringEscape; import diet.parser; import diet.dom; import std.algorithm.sorting : sort; import std.file; import std.path; import std.stdio; int main(string[] args) { if (args.length < 2) { writefln("USAGE: %s [ [...]]\n"); return 1; } void[0][TranslationKey] key_set; void collectRec(Node n) { if (n.translationKey.length) { auto template_name = n.loc.file.baseName.stripExtension; key_set[TranslationKey(n.translationKey, template_name)] = (void[0]).init; } foreach (nc; n.contents) if (nc.kind == NodeContent.Kind.node) collectRec(nc.node); } foreach (dir; args[1 .. $]) { foreach (de; dirEntries(dir, SpanMode.shallow)) { InputFile f; f.name = de.name.baseName; f.contents = (cast(char[])read(de.name)).idup; //auto inputs = rtGetInputs(de.name.baseName, args[1 .. $]); auto nodes = parseDietRaw!identity(f); foreach (n; nodes) collectRec(n); } } auto keys = key_set.keys; keys.sort!((a, b) { if (a.context != b.context) return a.context < b.context; if (a.text != b.text) return a.text < b.text; //if (a.mtext != b.mtext) return a.mtext < b.mtext; return false; }); writeln("msgid \"\""); writeln("msgstr \"\""); writeln("\"Content-Type: text/plain; charset=UTF-8\\n\""); writeln("\"Content-Transfer-Encoding: 8bit\\n\""); foreach (key; keys) { writefln("\nmsgctxt \"%s\"", dstringEscape(key.context)); writefln("msgid \"%s\"", dstringEscape(key.text)); writeln("msgstr \"\""); } return 0; } struct TranslationKey { string text; string context; } diet-ng-1.8.1/makepot/dub.sdl0000644000175000017500000000034614230473643015325 0ustar nileshnileshname "makepot" description "Generates a POT translation template from a set of Diet templates" authors "Sönke Ludwig" copyright "Copyright © 2021 Sönke Ludwig" license "MIT" targetName "makepot" dependency "diet-ng" path=".." diet-ng-1.8.1/CHANGELOG.md0000644000175000017500000002206614230473643014223 0ustar nileshnileshChangelog ========= v1.8.0 - 2021-07-27 ------------------- - The translation callback can now take an optional `context` parameter with the name of the source template - [pull #92][issue92] - Added a "makepot" tool to extract translation keys from a set of Diet template files - [pull #92][issue92] [issue92]: https://github.com/rejectedsoftware/diet-ng/issues/92 v1.7.4 - 2020-09-03 ------------------- - Fix documentation build and update test settings - [issue #87][issue87] [issue87]: https://github.com/rejectedsoftware/diet-ng/issues/87 v1.7.3 - 2020-09-02 ------------------- - Fix a deprecated Nullable alias this instance - [issue #84][issue84] - Add support for DMD 2.094 `-preview=in` switch - [issue #85][issue85] - Update release notes & meson build for v1.7.3 - [issue #86][issue86] [issue84]: https://github.com/rejectedsoftware/diet-ng/issues/84 [issue85]: https://github.com/rejectedsoftware/diet-ng/issues/85 [issue86]: https://github.com/rejectedsoftware/diet-ng/issues/86 v1.7.2 - 2020-03-25 ------------------- - Add back in import for `std.conv.to` - [issue #82][issue82] [issue82]: https://github.com/rejectedsoftware/diet-ng/issues/82 v1.7.1 - 2020-03-24 ------------------- - Fixed an issue where the translation callback had to be marked `@safe` - [pull #80][issue80] - Updates the Meson version number of the package - [issue #79][issue79], [pull #80][issue80] [issue79]: https://github.com/rejectedsoftware/diet-ng/issues/79 [issue80]: https://github.com/rejectedsoftware/diet-ng/issues/80 v1.7.0 - 2020-03-24 ------------------- - Adds support for a new "live mode" (by Steven Schveighoffer) - [pull #70][issue70], [pull #78][issue78] - Enabled by defining a version `DietUseLive` - Allows changes to the template to be reflected immediately at runtime - Only pure HTML changes are supported, changing embedded code will require a re-compile - Can greatly reduce the edit cycle during development - should not be used for production builds - Avoids redundant template compilations for templats instantiated with the same parameters (by Steven Schveighoffer) - [pull #77][issue77] - Fixed a possible range violation error (by Steven Schveighoffer) - [issue #75][issue75], [pull #76][issue76] [issue70]: https://github.com/rejectedsoftware/diet-ng/issues/70 [issue75]: https://github.com/rejectedsoftware/diet-ng/issues/75 [issue76]: https://github.com/rejectedsoftware/diet-ng/issues/76 [issue77]: https://github.com/rejectedsoftware/diet-ng/issues/77 [issue78]: https://github.com/rejectedsoftware/diet-ng/issues/78 v1.6.1 - 2019-10-25 ------------------- - Fixes the "transitional" HTML doctype string (by WebFreak) - [pull #60][issue60] - Compiles without deprecation warnings on DMD 2.088.0 - [pull #66][issue66] - Fixes the use of C++ style line comments in code lines - [issue #58][issue58], [pull #73][issue73] - Avoids excessive CTFE stack traces when syntax errors are encountered - [issue #69][issue69], [pull #73][issue73] [issue58]: https://github.com/rejectedsoftware/diet-ng/issues/58 [issue60]: https://github.com/rejectedsoftware/diet-ng/issues/60 [issue66]: https://github.com/rejectedsoftware/diet-ng/issues/66 [issue69]: https://github.com/rejectedsoftware/diet-ng/issues/69 [issue73]: https://github.com/rejectedsoftware/diet-ng/issues/73 v1.6.0 - 2019-08-16 ------------------- - Adds the new "extension includes" feature, combining blocks/extensions with includes - [pull #64][issue64] - Adds `Node.clone` and `NodeContent.clone` for recursive DOM cloning - [pull #64][issue64] - Updates compiler support to DMD 2.082.1 up to 2.087.1 and LDC 1.12.0 up to 1.16.0 - [pull #64][issue64] [issue64]: https://github.com/rejectedsoftware/diet-ng/issues/64 v1.5.0 - 2018-06-10 ------------------- - Adds `Node.translationKey` to allow external code to access the original translation key for translated nodes - [pull #55][issue55] [issue55]: https://github.com/rejectedsoftware/diet-ng/issues/55 v1.4.5 - 2018-03-12 ------------------- - Avoid singular tag enforcement for non-HTML documents - [issue #45][issue45], [pull #49][issue49] - Always output empty XML elements using singular tag syntax - [pull #50][issue50] - Fix broken XML doctype string (by Nicholas Wilson) - [pull #47][issue47] - Fix deprecation warnings on DMD 2.079.0 Note: 1.4.3 and 1.4.4 just bumped the used vibe.d version of the examples. [issue45]: https://github.com/rejectedsoftware/diet-ng/issues/45 [issue47]: https://github.com/rejectedsoftware/diet-ng/issues/47 [issue49]: https://github.com/rejectedsoftware/diet-ng/issues/49 [issue50]: https://github.com/rejectedsoftware/diet-ng/issues/50 v1.4.2 - 2017-08-26 ------------------- - Fixes "variable val is shadowing variable" errors when defining a variable `val` in a template - [issue #35][issue35] - Fixes missing escaping for quotes in HTML attributes that are not typed `bool` or `string` - [issue #36][issue36] - Tweaked the Meson build description to be usable as a sub project - [pull #39][issue39] [issue35]: https://github.com/rejectedsoftware/diet-ng/issues/35 [issue36]: https://github.com/rejectedsoftware/diet-ng/issues/36 [issue39]: https://github.com/rejectedsoftware/diet-ng/issues/39 v1.4.1 - 2017-08-20 ------------------- - Adds a Meson project description (by Matthias Klumpp aka ximion) - [pull #37][issue37] [issue37]: https://github.com/rejectedsoftware/diet-ng/issues/37 v1.4.0 - 2017-08-13 ------------------- - Implemented support for multi-line nodes (by Jan Jurzitza aka WebFreak) - [issue vibe.d#1307][issue1307_vibe.d] - The shortcut syntax for class/id attributes is now allowed to start with a digit - [issue #32][issue32] [issue32]: https://github.com/rejectedsoftware/diet-ng/issues/32 [issue1307_vibe.d]: https://github.com/rejectedsoftware/vibe.d/issues/1307 v1.3.0 - 2017-07-23 ------------------- - Heavily reduced the length of template symbol named generated during compilation, resulting in a lot less binary bloat - Added support for a `.processors` field in traits structs that contains a list or arbitrary DOM modification functions - Add DOM manipulation convenience functions v1.2.1 - 2017-04-18 ------------------- - Fixed/implemented HTML white space inhibition using the `<`/`>` suffixes - [issue #27][issue27] [issue27]: https://github.com/rejectedsoftware/diet-ng/issues/27 v1.2.0 - 2017-03-02 ------------------- - Added `compileHTMLDietFileString`, a variant of `compileHTMLDietString` that can make use of includes and extensions - [issue #24][issue24] - Fixed a compile error for filter nodes and output ranges that are not `nothrow` - Fixed extraneous newlines getting inserted in front of HTML text nodes when pretty printing was enabled [issue24]: https://github.com/rejectedsoftware/diet-ng/issues/24 v1.1.4 - 2017-02-23 ------------------- - Fixes formatting of singluar elements in pretty HTML output - [issue #18][issue18] - Added support for Boolean attributes that are sourced from a property/implicit function call (by Sebastian Wilzbach) - [issue #19][issue19], [pull #20][issue20] [issue18]: https://github.com/rejectedsoftware/diet-ng/issues/18 [issue19]: https://github.com/rejectedsoftware/diet-ng/issues/19 [issue20]: https://github.com/rejectedsoftware/diet-ng/issues/20 v1.1.3 - 2017-02-09 ------------------- ### Bug fixes ### - Works around an internal compiler error on 2.072.2 that got triggered in 1.1.2 v1.1.2 - 2017-02-06 ------------------- ### Features and improvements ### - Class/ID definitions (`.cls#id`) can now be specified in any order - [issue #9][issue9] - Block definitions can now also be in included files - [issue #14][issue14] - Multiple contents definitions for the same block are now handled properly - [issue #13][issue13] [issue9]: https://github.com/rejectedsoftware/diet-ng/issues/9 [issue13]: https://github.com/rejectedsoftware/diet-ng/issues/13 [issue14]: https://github.com/rejectedsoftware/diet-ng/issues/14 v1.1.1 - 2016-12-19 ------------------- ### Bug fixes ### - Fixed parsing of empty lines in raw text blocks v1.1.0 - 2016-09-29 ------------------- This release adds support for pretty printing and increases backwards compatibility with older DMD front end versions. ### Features and improvements ### - Compiles on DMD 2.068.0 up to 2.071.2 - Supports pretty printed HTML output by inserting a `htmlOutputStyle` field in a traits struct - [issue #8][issue8] [issue8]: https://github.com/rejectedsoftware/diet-ng/issues/8 v1.0.0 - 2016-09-22 ------------------- This is the first stable release of diet-ng. Compared to the original `vibe.templ.diet` module in vibe.d, it offers a large number of improvements. ### Features and improvements ### - No external dependencies other than Phobos - Extensible/configurable with traits structures - Supports inline and nested tags syntax - Supports string interpolations within filter nodes (falls back to runtime filters) - Supports arbitrary uses other than generating HTML, for example we use it similar to QML/XAML for our internal UI framework - The API is `@safe` and `nothrow` where possible - Uses less memory during compilation - Comprehensive unit test suite used throughout development - Supports AngularJS special attribute names diet-ng-1.8.1/README.md0000644000175000017500000001653314230473643013673 0ustar nileshnileshDiet-NG ======= Diet is a generic compile-time template system based on an XML-like structure. The syntax is heavily influenced by [pug](https://pugjs.org/) (formerly "Jade") and [Haml](http://haml.info/) and outputting dynamic HTML is the primary goal. It supports pluggable transformation modules, as well as output modules, so that many other uses are possible. See the preliminary [Specification](SPEC.md) for a syntax overview. This repository contains the designated successor implementation of the [`vibe.templ.diet` module](https://vibed.org/api/vibe.templ.diet/) of [vibe.d](https://vibed.org/). The current state is almost stable and feature complete and ready for pre-production testing. [![DUB link](https://img.shields.io/dub/v/diet-ng.svg)](https://code.dlang.org/packages/diet-ng) [![Build Status](https://github.com/rejectedsoftware/diet-ng/actions/workflows/ci.yml/badge.svg)](https://github.com/rejectedsoftware/diet-ng/actions/workflows/ci.yml) Example ------- doctype html - auto title = "Hello, "; html head title #{title} - example page body h1= title h2 Index ol.pageindex - foreach (i; 0 .. 3) li: a(href="##{i}") Point #{i} - foreach (i; 0 .. 3) h2(id=i) Point #{i} p. These are the #[em contents] of point #{i}. Multiple lines of text are contained in this paragraph. Generated HTML output: Hello, <World> - example page

Hello, <World>

Index

  1. Point 0
  2. Point 1
  3. Point 2

Point 0

These are the contents of point 0. Multiple lines of text are contained in this paragraph.

Point 1

These are the contents of point 1. Multiple lines of text are contained in this paragraph.

Point 2

These are the contents of point 2. Multiple lines of text are contained in this paragraph.

Implementation goals -------------------- - Be as fast as possible. This means moving as many operations from run time to compile time as possible. - Avoid any dynamic memory allocations (unless it happens in user code) - Let the generated code be fully `@safe` (unless embedded user code isn't) - Be customizable (filters, translation, DOM transformations, output generators), without resorting to global library state - Operate on ranges. HTML output is written to an output range, input ranges are supported within string interpolations and filters/translation support is supposed to be implementable using ranges (the latter part is not yet implemented). Experimental HTML template caching ---------------------------------- Since compiling complex Diet templates can slow down the overall compilation process, the library provides an option to cache and re-use results. It is enabled by defining the version constant `DietUseCache` ( `"versions": ["DietUseCache"]` in dub.json or `versions "DietUseCache"` in dub.sdl). It is not recommended to use this feature outside of the usual edit-compile-run development cycle, especially not for release builds. Once enabled, the template compiler will look for `*_cached_#####.d` files in the "views/" folder, where the `*` consists of the full path of the Diet template and `#####` represents a unique hash value that identifies the contents of the template, as well as included/extended ones. If found, it will simply use the contents of that file instead of going through the whole compilation process. At runtime, during initialization, the program will then output the contents of all newly compiled templates to the "views/" folder. For that reason it is currently **important that the program is run with the current working directory set to the package directory!** A drawback of this method is that outdated cached templates will not be deleted automatically. It is necessary to clear all `*_cached_*.d` files by hand from time to time. *Note that hopefully this feature will be obsoleted soon by the [work of Stefan Koch on DMD's CTFE engine](https://github.com/UplinkCoder/dmd/commits/newCTFE).* Experimental HTML Live Mode ---------------------------------- Building a diet template at compile-time can be slow, as mentioned above. A major drawback of this is that during development, any single change to any diet file requires a complete rebuild of the entire project. The library now supports a "Live Mode", where any changes to the templates that are strictly HTML related will be rendered on a page refresh, instead of requiring a recompilation. This works by replacing output of the HTML portions of the template with output from a lookup table of strings. Then the strings are rebuilt whenever the file changes. So for example, adding or removing a class from an html element, or fixing `herf` to `href` in an anchor element does not require a recompile. Changes to code portions of the template (i.e. string interpolations such as `#{expression}` or `!{expression}`, or any D code escapes such as `- foreach(x; range)`) will throw an exception, and force you to recompile your project before continuing. This is because the diet engine can deal with changes to string data, but cannot recompile your project for you. And obviously, changing code outside the templates will not change the rendered pages without a recompile. Even adding new lines or inserting lines where HTML did not exist is supported. For example: ```pug - if(cond) - auto a = foobar(); ``` changed to the following will not require a recompile ```pug - if(cond) a(href="/") Home - auto a = foobar(); ``` The mode is enabled by defining the version constant `DietUseLive` ( `"versions": ["DietUseLive"]` in dub.json or `versions "DietUseLive"` in dub.sdl). It is not recommended to use this in production for the same reasons listed for the caching mode. To be as efficient as possible, the templates are only parsed on first access, and re-parsed only when a modification in any template or dependent template is detected. Note that it still will not be as efficient as the normal mode which doesn't require any file i/o to render templates. There are a few limitations to this approach. Like `DietUseCache`, this REQUIRES the views directory to be accessible to the running executable. In addition, to keep the code generation simple (and avoid a full D parser), certain features do not work with Live Mode. Two such features are type definitions (i.e. structs, unions, or classes), and static functions. There is no escape mechanism to allow these, so you will have to ensure that they are not present in your diet templates, or you will get probably very strange compiler errors. Any other problems, please report them in github. This mode and the `DietUseCache` mode can be combined. Just define both versions in your project's dub configuration. Examples Directory ------------------ The examples directory contains 2 projects showcasing the features of diet. * `htmlgenerator` - Uses diet-ng to generate static html files from diet templates. * `htmlserver` - Simple vibe.d project that shows some features of diet template parsing. Note that there are multiple configurations that show how the caching and live mode work. Please see the README.md file for more details in that directory. diet-ng-1.8.1/dub.sdl0000644000175000017500000000033314230473643013661 0ustar nileshnileshname "diet-ng" description "Next generation Diet template compiler." authors "Sönke Ludwig" copyright "Copyright © 2015-2016 Sönke Ludwig" license "MIT" x:ddoxFilterArgs "--ex" "diet.internal." subPackage "makepot"diet-ng-1.8.1/test.sh0000755000175000017500000000050214230473643013717 0ustar nileshnilesh#!/bin/bash set -xe DC=${DC%-*} if [ "$DC" == "ldc" ]; then DC="ldc2"; fi echo "Running unit tests..." dub test echo "Checing makepot for successful compilation..." dub build :makepot if [ "$DC" == "ldc2" ]; then echo "Testing for DIP 1000 compatibility..." DFLAGS="--preview=dip1000 --preview=dip25" dub build fi