pax_global_header00006660000000000000000000000064132436207740014522gustar00rootroot0000000000000052 comment=1e6be1a77986a4a4d223b9a4c06546fc16fd02ce diet-ng-1.4.4/000077500000000000000000000000001324362077400130575ustar00rootroot00000000000000diet-ng-1.4.4/.editorconfig000066400000000000000000000002441324362077400155340ustar00rootroot00000000000000root = 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.4.4/.gitignore000066400000000000000000000004261324362077400150510ustar00rootroot00000000000000*.[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.4.4/.travis.yml000066400000000000000000000022631324362077400151730ustar00rootroot00000000000000language: d sudo: false dist: trusty addons: apt: packages: - pkg-config - zlib1g-dev - libevent-dev - libssl-dev d: # order: latest DMD, oldest DMD, LDC/GDC, remaining DMD versions # this way the overall test time gets cut down (GDC/LDC are a lot # slower tham DMD, so they should be started early), while still # catching most DMD version related build failures early - dmd-2.078.1 - dmd-2.072.2 - ldc-1.7.0 - ldc-1.6.0 - ldc-1.5.0 - ldc-1.4.0 - ldc-1.3.0 - ldc-1.2.0 - dmd-2.077.1 - dmd-2.076.1 - dmd-2.075.1 - dmd-2.074.1 - dmd-2.073.2 - dmd-2.072.2 #before_install: #- pyenv global system 3.6 #- pip3 install meson>=0.40 #install: #- mkdir .ntmp #- curl -L https://github.com/ninja-build/ninja/releases/download/v1.7.2/ninja-linux.zip -o .ntmp/ninja-linux.zip #- unzip .ntmp/ninja-linux.zip -d .ntmp #before_script: #- export PATH=$PATH:$PWD/.ntmp script: - dub build -b release - dub test - dub build --root examples/htmlgenerator - dub build --root examples/htmlserver # test building with Meson - mkdir build && cd build #- meson .. #- ninja -j4 #- DESTDIR=/tmp/diet_inst_target ninja install diet-ng-1.4.4/CHANGELOG.md000066400000000000000000000110311324362077400146640ustar00rootroot00000000000000Changelog ========= 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.4.4/CONTRIBUTING.md000066400000000000000000000014711324362077400153130ustar00rootroot00000000000000Guidelines 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.4.4/LICENSE.txt000066400000000000000000000020551324362077400147040ustar00rootroot00000000000000Copyright (c) 2012-2014 RejectedSoftware e.K. 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.4.4/README.md000066400000000000000000000102161324362077400143360ustar00rootroot00000000000000Diet-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://travis-ci.org/rejectedsoftware/diet-ng.svg?branch=master)](https://travis-ci.org/rejectedsoftware/diet-ng) 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 file name of the Diet template and 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_*` 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).* diet-ng-1.4.4/SPEC.md000066400000000000000000000301711324362077400141350ustar00rootroot00000000000000Diet 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 --------------------- `extend file(.ext)` **TODO!** 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.4.4/dub.sdl000066400000000000000000000003161324362077400143350ustar00rootroot00000000000000name "diet-ng" description "Next generation Diet template compiler." authors "Sönke Ludwig" copyright "Copyright © 2015-2016 rejectedsoftware e.K." license "MIT" x:ddoxFilterArgs "--ex" "diet.internal." diet-ng-1.4.4/examples/000077500000000000000000000000001324362077400146755ustar00rootroot00000000000000diet-ng-1.4.4/examples/htmlgenerator/000077500000000000000000000000001324362077400175505ustar00rootroot00000000000000diet-ng-1.4.4/examples/htmlgenerator/dub.sdl000066400000000000000000000000671324362077400210310ustar00rootroot00000000000000name "htmlgenerator" dependency "diet-ng" path="../.." diet-ng-1.4.4/examples/htmlgenerator/source/000077500000000000000000000000001324362077400210505ustar00rootroot00000000000000diet-ng-1.4.4/examples/htmlgenerator/source/app.d000066400000000000000000000003541324362077400217770ustar00rootroot00000000000000import 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.4.4/examples/htmlgenerator/views/000077500000000000000000000000001324362077400207055ustar00rootroot00000000000000diet-ng-1.4.4/examples/htmlgenerator/views/index.dt000066400000000000000000000006541324362077400223520ustar00rootroot00000000000000doctype 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.4.4/examples/htmlserver/000077500000000000000000000000001324362077400170705ustar00rootroot00000000000000diet-ng-1.4.4/examples/htmlserver/dub.sdl000066400000000000000000000001751324362077400203510ustar00rootroot00000000000000name "htmlserver" dependency "diet-ng" path="../.." dependency "vibe-d" version="~>0.8.3-alpha.1" versions "VibeDefaultMain" diet-ng-1.4.4/examples/htmlserver/source/000077500000000000000000000000001324362077400203705ustar00rootroot00000000000000diet-ng-1.4.4/examples/htmlserver/source/app.d000066400000000000000000000006651324362077400213240ustar00rootroot00000000000000import diet.html; 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); } shared static this() { auto settings = new HTTPServerSettings; settings.bindAddresses = ["::1", "127.0.0.1"]; settings.port = 8080; listenHTTP(settings, &render); } diet-ng-1.4.4/examples/htmlserver/views/000077500000000000000000000000001324362077400202255ustar00rootroot00000000000000diet-ng-1.4.4/examples/htmlserver/views/index.dt000066400000000000000000000006541324362077400216720ustar00rootroot00000000000000doctype 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.4.4/meson.build000066400000000000000000000025151324362077400152240ustar00rootroot00000000000000project('Diet-NG', 'd', meson_version: '>=0.40', license: 'MIT', version: '1.4.1' ) 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.4.4/source/000077500000000000000000000000001324362077400143575ustar00rootroot00000000000000diet-ng-1.4.4/source/diet/000077500000000000000000000000001324362077400153045ustar00rootroot00000000000000diet-ng-1.4.4/source/diet/defs.d000066400000000000000000000017021324362077400163720ustar00rootroot00000000000000/** 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 ref Location loc) { 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.4.4/source/diet/dom.d000066400000000000000000000316031324362077400162330ustar00rootroot00000000000000/** 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; 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; } 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; /// Constructs a new node. this(Location loc = Location.init, string name = null, Attribute[] attributes = null, NodeContent[] contents = null, NodeAttribs attribs = NodeAttribs.none) { this.loc = loc; this.name = name; this.attributes = attributes; this.contents = contents; this.attribs = attribs; } /// 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"); } /** 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 ref 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)", 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(in Node other) 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); } /// Compares node content for equality. bool opEquals(in ref NodeContent other) const { 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.4.4/source/diet/html.d000066400000000000000000000615041324362077400164230ustar00rootroot00000000000000/** 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; /** Compiles a Diet template file that is available as a string import. The final HTML will be written to the given `_diet_output` output range. 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. dst = The output range to write the generated HTML to. 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` */ template compileHTMLDietFile(string filename, ALIASES...) { import diet.internal.string : stripUTF8BOM; private static immutable contents = stripUTF8BOM(import(filename)); alias compileHTMLDietFile = compileHTMLDietFileString!(filename, contents, ALIASES); } /** 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. dst = The output range to write the generated HTML to. See_Also: `compileHTMLDietFile`, `compileHTMLDietString`, `compileHTMLDietStrings` */ template compileHTMLDietFileString(string filename, alias contents, ALIASES...) { import std.conv : to; enum _diet_files = collectFiles!(filename, contents); version (DietUseCache) enum _diet_use_cache = true; else enum _diet_use_cache = false; 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 = "_cached_"~filename~"_"~_diet_hash.to!string~".d"; 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 { alias TRAITS = DietTraits!ALIASES; pragma(msg, "Compiling Diet HTML template "~filename~"..."); private Document _diet_nodes() { return applyTraits!TRAITS(parseDiet!(translate!TRAITS)(_diet_files)); } 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); } } } // 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); } 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); } } /** Returns a mixin string that generates HTML for the given DOM tree. Params: nodes = The root nodes of the DOM tree range_name = Optional custom name to use for the output range, defaults to `_diet_output`. 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 = "import diet.internal.html : htmlEscape, htmlAttribEscape;\n"; ret ~= "import std.format : formattedWrite;\n"; foreach (i, n; doc.nodes) ret ~= ctx.getHTMLMixin(n, false); ret ~= ctx.flushRawText(); return ret; } unittest { import diet.parser; void test(string src)(string expected) { import std.array : appender; 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!"doctype html\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"("
"); } /** 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) { 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) { 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 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; } // 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; } ret ~= ctx.statement(node.loc, q{ static if (is(typeof(() { return %s; }()) == bool) ) }~'{', expr); if (ctx.isHTML5) ret ~= ctx.statement(node.loc, q{if (%s) %s.put(" %s");}, expr, ctx.rangeName, att.name); else ret ~= ctx.statement(node.loc, q{if (%s) %s.put(" %s=\"%s\"");}, expr, ctx.rangeName, att.name, att.name); ret ~= ctx.statement(node.loc, "} else "~q{static if (is(typeof(%s) : const(char)[])) }~"{{", expr); ret ~= ctx.statement(node.loc, q{ auto _diet_val = %s;}, expr); ret ~= ctx.statement(node.loc, q{ if (_diet_val !is null) }~'{'); ret ~= ctx.rawText(node.loc, " "~att.name~"=\""); ret ~= ctx.statement(node.loc, q{ %s.filterHTMLAttribEscape(_diet_val);}, ctx.rangeName); ret ~= ctx.rawText(node.loc, "\""); ret ~= ctx.statement(node.loc, " }"); ret ~= ctx.statement(node.loc, "}} else {"); } ret ~= ctx.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) { 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) { 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" ?`; 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; break; } return ctx.rawText(node.loc, "<"~dstringEscape(doctype_str)~">"); } private string getCodeMixin(ref CTX ctx, in ref Node node, bool in_pre) { 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 got_code = false; foreach (i, c; node.contents) { if (i == 0 && c.kind == NodeContent.Kind.text) { ret ~= ctx.statement(node.loc, "%s {", c.value); got_code = true; } else { assert(c.kind == NodeContent.Kind.node); ret ~= ctx.getHTMLMixin(c.node, in_pre); } } ret ~= ctx.statement(node.loc, "}"); return ret; } private string getCommentMixin(ref CTX ctx, in ref Node node) { string ret = ctx.rawText(node.loc, ""); return ret; } private struct CTX { enum NewlineState { none, plain, pretty, inhibit } bool isHTML5; bool pretty; int depth = 0; string rangeName; bool inRawText = false; NewlineState newlineState = NewlineState.none; bool anyText; pure string statement(ARGS...)(Location loc, string fmt, ARGS args) { import std.string : format; string ret = flushRawText(); ret ~= ("#line %s \"%s\"\n"~fmt~"\n").format(loc.line+1, loc.file, args); 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) { ret = this.rangeName ~ ".put(\""; this.inRawText = true; } ret ~= outputPendingNewline(); ret ~= dstringEscape(text); anyText = true; return ret; } pure string flushRawText() { if (this.inRawText) { this.inRawText = false; 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; 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!(`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"); } diet-ng-1.4.4/source/diet/input.d000066400000000000000000000072621324362077400166170ustar00rootroot00000000000000/** 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; /** 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; } diet-ng-1.4.4/source/diet/internal/000077500000000000000000000000001324362077400171205ustar00rootroot00000000000000diet-ng-1.4.4/source/diet/internal/html.d000066400000000000000000000137161324362077400202410ustar00rootroot00000000000000/** 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; 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.4.4/source/diet/internal/string.d000066400000000000000000000032201324362077400205700ustar00rootroot00000000000000module 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 ref 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.4.4/source/diet/parser.d000066400000000000000000001314211324362077400167470ustar00rootroot00000000000000/** 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; /** 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)) { InputFile[1] f; f[0].name = filename; f[0].contents = text; return parseDiet!TR(f); } Document parseDiet(alias TR = identity)(InputFile[] files) if (is(typeof(TR(string.init)) == string)) { import diet.traits; import std.algorithm.iteration : map; import std.array : array; FileInfo[] parsed_files = files.map!(f => FileInfo(f.name, parseDietRaw!TR(f))).array; BlockInfo[] blocks; return new Document(parseDietWithExtensions(parsed_files, 0, blocks, null)); } unittest { // test basic functionality Location ln(int l) { return Location("string", l); } // simple node assert(parseDiet("test").nodes == [ new Node(ln(0), "test") ]); // nested nodes assert(parseDiet("foo\n bar").nodes == [ 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 == [ 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 == [ // 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 == [ new Node(ln(0), "", [ Attribute(ln(0), "class", [AttributeContent.text("foo")]) ]) ]); assert(parseDiet("a.download-button\n\t.bs-hbtn.right.black").nodes == [ 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 == [ new Node(ln(0), "", [ Attribute(ln(0), "id", [AttributeContent.text("foo")]) ]) ]); // node with attributes assert(parseDiet("test(foo1=\"bar\", foo2=2+3)").nodes == [ 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 == [ 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 == [ 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 == [ new Node(ln(0), "foo", null, [ NodeContent.text("test", ln(0)) ], NodeAttribs.translated) ]); // interpolated text assert(parseDiet("foo hello #{\"world\"} #bar \\#{baz}").nodes == [ 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 == [ new Node(ln(0), "foo", null, [ NodeContent.interpolation(`1+2`, ln(0)), ]) ]); // expression with empty tag name assert(parseDiet("= 1+2").nodes == [ new Node(ln(0), "", null, [ NodeContent.interpolation(`1+2`, ln(0)), ]) ]); // raw expression assert(parseDiet("foo!= 1+2").nodes == [ new Node(ln(0), "foo", null, [ NodeContent.rawInterpolation(`1+2`, ln(0)), ]) ]); // interpolated attribute text assert(parseDiet("foo(att='hello #{\"world\"} #bar')").nodes == [ 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 == [ 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 == [ 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 == [ new Node(ln(0), Node.SpecialName.comment, null, [NodeContent.text("comment", ln(0))], NodeAttribs.rawTextNode) ]); assert(parseDiet("//-hide").nodes == [ new Node(ln(0), Node.SpecialName.hidden, null, [NodeContent.text("hide", ln(0))], NodeAttribs.rawTextNode) ]); assert(parseDiet("!!! 5").nodes == [ new Node(ln(0), "doctype", null, [NodeContent.text("5", ln(0))]) ]); assert(parseDiet("").nodes == [ new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text("", ln(0))]) ]); assert(parseDiet("|text").nodes == [ new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text("text", ln(0))]) ]); assert(parseDiet("|text\n").nodes == [ new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text("text", ln(0))]) ]); assert(parseDiet("| text\n").nodes == [ new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text("text", ln(0))]) ]); assert(parseDiet("|.").nodes == [ new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text(".", ln(0))]) ]); assert(parseDiet("|:").nodes == [ new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text(":", ln(0))]) ]); assert(parseDiet("|&x").nodes == [ new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text("x", ln(0))], NodeAttribs.translated) ]); assert(parseDiet("-if(x)").nodes == [ new Node(ln(0), Node.SpecialName.code, null, [NodeContent.text("if(x)", ln(0))]) ]); assert(parseDiet("-if(x)\n\t|bar").nodes == [ 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 == [ new Node(ln(0), ":", [Attribute(ln(0), "filterChain", [AttributeContent.text("foo")])], [ NodeContent.text("bar", ln(1)) ], NodeAttribs.textNode) ]); assert(parseDiet(":foo :bar baz").nodes == [ 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 == [ 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 == [ 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 == [ new Node(ln(0), "a", null, [ NodeContent.tag(new Node(ln(0), "b")) ]) ]); assert(parseDiet("a: b\n\tc\nd").nodes == [ 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 == [ new Node(ln(0), "a", null, [ NodeContent.tag(new Node(ln(0), "b")) ]) ]); assert(parseDiet("a #[b #[c d]]").nodes == [ 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 == [ new Node(ln(0), "a", null, [], NodeAttribs.fitInside|NodeAttribs.fitOutside) ]); assert(parseDiet("a><").nodes == [ new Node(ln(0), "a", null, [], NodeAttribs.fitInside|NodeAttribs.fitOutside) ]); assert(parseDiet("a<").nodes == [ new Node(ln(0), "a", null, [], NodeAttribs.fitInside) ]); assert(parseDiet("a>").nodes == [ new Node(ln(0), "a", null, [], NodeAttribs.fitOutside) ]); } unittest { Location ln(int l) { return Location("string", l); } // angular2 html attributes tests assert(parseDiet("div([value]=\"firstName\")").nodes == [ new Node(ln(0), "div", [ Attribute(ln(0), "[value]", [ AttributeContent.text("firstName"), ]) ]) ]); assert(parseDiet("div([attr.role]=\"myRole\")").nodes == [ new Node(ln(0), "div", [ Attribute(ln(0), "[attr.role]", [ AttributeContent.text("myRole"), ]) ]) ]); assert(parseDiet("div([attr.role]=\"{foo:myRole}\")").nodes == [ new Node(ln(0), "div", [ Attribute(ln(0), "[attr.role]", [ AttributeContent.text("{foo:myRole}"), ]) ]) ]); assert(parseDiet("div([attr.role]=\"{foo:myRole, bar:MyRole}\")").nodes == [ 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 == [ new Node(ln(0), "div", [ Attribute(ln(0), "(attr.role)", [ AttributeContent.text("{foo:myRole, bar:MyRole}") ]) ]) ]); assert(parseDiet("div([class.extra-sparkle]=\"isDelightful\")").nodes == [ new Node(ln(0), "div", [ Attribute(ln(0), "[class.extra-sparkle]", [ AttributeContent.text("isDelightful") ]) ]) ]); auto t = parseDiet("div((click)=\"readRainbow($event)\")"); assert(t.nodes == [ new Node(ln(0), "div", [ Attribute(ln(0), "(click)", [ AttributeContent.text("readRainbow($event)") ]) ]) ]); assert(parseDiet("div([(title)]=\"name\")").nodes == [ new Node(ln(0), "div", [ Attribute(ln(0), "[(title)]", [ AttributeContent.text("name") ]) ]) ]); assert(parseDiet("div(*myUnless=\"myExpression\")").nodes == [ new Node(ln(0), "div", [ Attribute(ln(0), "*myUnless", [ AttributeContent.text("myExpression") ]) ]) ]); assert(parseDiet("div([ngClass]=\"{active: isActive, disabled: isDisabled}\")").nodes == [ 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 == [ 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 == [ new Node(ln(0), "div", [ Attribute(ln(0), "({*ngFor})", [ AttributeContent.text("{args:"), AttributeContent.text("#"), AttributeContent.text("item of list}") ]) ]) ]); } 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 == [ new Node(ln(0), "foo", null, [ NodeContent.text("(TEST)", ln(0)) ], NodeAttribs.translated) ]); assert(parseDiet!tr("foo& test #{x} it").nodes == [ new Node(ln(0), "foo", null, [ NodeContent.text("(TEST ", ln(0)), NodeContent.interpolation("X", ln(0)), NodeContent.text(" IT)", ln(0)), ], NodeAttribs.translated) ]); assert(parseDiet!tr("|&x").nodes == [ new Node(ln(0), Node.SpecialName.text, null, [NodeContent.text("(X)", ln(0))], NodeAttribs.translated) ]); assert(parseDiet!tr("foo&.\n\tbar\n\tbaz").nodes == [ new Node(ln(0), "foo", null, [ NodeContent.text("(BAR)", ln(1)), NodeContent.text("\n(BAZ)", ln(2)) ], NodeAttribs.translated|NodeAttribs.textNode) ]); } 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."); } 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") == [ 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"); testFail("include #{p}", "Dynamic includes are not supported."); testFail("include inc\n\tp", "Includes cannot have children."); testFail("p\ninclude inc\n\tp", "Includes cannot have children."); } 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") == [ new Node(Location("root.dt", 0), "html", null, null) ]); assert(parse("extends root\nblock a\n\tdiv\nblock b\n\tpre") == [ 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") == [ 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") == [ 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") == [ 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") == [ 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") == [ // 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") == [ 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") == []); assert(parse("extends direct\nblock a\n\tp") == [ new Node(Location("main.dt", 2), "p", null, null) ]); } 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); } unittest { // regression tests Location ln(int l) { return Location("string", l); } // last line contains only whitespace assert(parseDiet("test\n\t").nodes == [ new Node(ln(0), "test") ]); } 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 == [ 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) ]); } unittest { // issue #32 - numeric id/class Location ln(int l) { return Location("string", l); } assert(parseDiet("foo.01#02").nodes == [ 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) nothrow @safe @nogc { return str; } private string parseIdent(in ref string str, ref size_t start, string breakChars, in ref Location loc) { 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(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); } private Node[] parseDietWithExtensions(FileInfo[] files, size_t file_index, ref BlockInfo[] blocks, size_t[] import_stack) { 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); auto 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); } // parse base template return parseDietWithExtensions(files, base_idx, blocks, import_stack ~ file_index); } static string extractFilename(Node n) { 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) { Nullable!(Node[]) ret; void insert(Node[] nodes) { foreach (i, n; nodes) { auto np = processNode(n); if (!np.isNull()) { if (ret.isNull) ret = nodes[0 .. i]; ret ~= np; } else if (!ret.isNull) ret ~= 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); enforcep(n.contents.length == 1, "Includes cannot have children.", n.loc); auto fidx = files.countUntil!(f => matchesName(f.name, name, files[file_index].name)); enforcep(fidx >= 0, "Missing include input file: "~name, n.loc); insert(parseDietWithExtensions(files, fidx, blocks, import_stack ~ file_index)); } else { n.contents.modifyArray!((nc) { Nullable!(NodeContent[]) rn; if (nc.kind == NodeContent.Kind.node) { auto mod = processNode(nc.node); if (!mod.isNull()) rn = mod.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.modifyArray!(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 (indent.length) pnode.addText(indent, loc); if (pnode.attribs & NodeAttribs.translated) { size_t idx; Location loccopy = loc; auto ln = TR(skipLine(input, idx, loc)); input = input[idx .. $]; parseTextLine(ln, pnode, loccopy); } else parseTextLine(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(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(input, ret, loc); return ret; } if (input.startsWith('<')) { // inline HTML/XML ret.name = Node.SpecialName.text; parseTextLine(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 .. $]; if (ret.attribs & NodeAttribs.translated) remainder = TR(remainder); parseTextLine(remainder, ret, tmploc); } else if (ret.name == Node.SpecialName.text) { // allow omitting the whitespace for "|" text nodes if (ret.attribs & NodeAttribs.translated) remainder = TR(remainder); parseTextLine(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) { 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(ref string input, ref Node dst, ref Location loc) { import std.algorithm.comparison : among; size_t sidx = 0, idx = 0; void flushText() { 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) { 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) { 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 ref Location loc) { 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 ref Location loc) { 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 ref string s, ref size_t idx, in ref Location loc) { 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 ref string s, ref size_t idx, in ref Location loc) { 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 ref string s, ref size_t idx, string additional_chars, in ref Location loc, bool accept_empty = false, bool require_alpha_start = false) { 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) { 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) { return ch == ' ' || ch == '\t'; } private string skipAnyWhitespace(in ref string s, ref size_t idx) { 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) { 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; } 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 ref string s, ref size_t idx, in ref Location loc, bool multiline = false) { 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 ref string s, ref size_t idx, char delimiter, in ref Location loc) { 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) { 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 void modifyArray(alias modify, T)(ref T[] arr) { size_t i = 0; while (i < arr.length) { auto mod = modify(arr[i]); if (mod.isNull()) i++; else { arr = arr[0 .. i] ~ mod.get() ~ arr[i+1 .. $]; i += mod.length; } } } diet-ng-1.4.4/source/diet/traits.d000066400000000000000000000244611324362077400167660ustar00rootroot00000000000000/** 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() { return DietTraitsAttribute.init; } /// 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) { import std.traits : hasUDA; foreach (T; TRAITS) { static assert(hasUDA!(T, DietTraitsAttribute)); static if (is(typeof(&T.translate))) text = T.translate(text); } return text; } /** Applies any transformations that are defined in the */ 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) { 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 ~ `}`; } 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); } private 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); }