pax_global_header 0000666 0000000 0000000 00000000064 15047124314 0014513 g ustar 00root root 0000000 0000000 52 comment=aae414692b6052e96d890e03bbeeeca0f4dc01c2
erlydtl-0.15.0/ 0000775 0000000 0000000 00000000000 15047124314 0013255 5 ustar 00root root 0000000 0000000 erlydtl-0.15.0/.github/ 0000775 0000000 0000000 00000000000 15047124314 0014615 5 ustar 00root root 0000000 0000000 erlydtl-0.15.0/.github/workflows/ 0000775 0000000 0000000 00000000000 15047124314 0016652 5 ustar 00root root 0000000 0000000 erlydtl-0.15.0/.github/workflows/main.yml 0000664 0000000 0000000 00000000646 15047124314 0020327 0 ustar 00root root 0000000 0000000 on: push
jobs:
test:
runs-on: ubuntu-24.04
name: Erlang/OTP ${{matrix.otp}} / rebar3 ${{matrix.rebar3}}
strategy:
matrix:
otp: ['26.2', '27.3', '28.0']
rebar3: ['3.24', '3.25']
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1.20.4
with:
otp-version: ${{matrix.otp}}
rebar3-version: ${{matrix.rebar3}}
- run: rebar3 eunit
erlydtl-0.15.0/.gitignore 0000664 0000000 0000000 00000000403 15047124314 0015242 0 ustar 00root root 0000000 0000000 ebin
*.swp
erl_crash.dump
examples/rendered_output
src/erlydtl_parser.erl
*~
ebintest
tests/src/erlydtl_extension_testparser.erl
tests/output
deps
.emacs*
.eunit
/.edts
/.rebar/
/*.beam
/.settings/
_build
/.project
include/yeccpre-*.hrl
src/erlydtl.app.src-*
erlydtl-0.15.0/CONTRIBUTING.md 0000664 0000000 0000000 00000005574 15047124314 0015521 0 ustar 00root root 0000000 0000000 Contributing to erlydtl
-----------------------
Before implementing a new feature, please submit a ticket to discuss
your plans. The feature might have been rejected already, or the
implementation might already be decided.
Code style
----------
The following rules must be followed:
* Do not introduce trailing whitespace
* Do not mix spaces and tabs
* Do not introduce lines longer than 120 characters
The following rules should be followed:
* Write small functions whenever possible
* Avoid having too many clauses containing clauses containing
clauses. Basically, avoid deeply nested functions.
[erlang-mode (emacs)](http://www.erlang.org/doc/man/erlang.el.html)
indentation is preferred. This will keep the code base consistent. vi
users are encouraged to give
[Vim emulation](http://emacswiki.org/emacs/Evil)
([more info](https://gitorious.org/evil/pages/Home)) a try.
Pull requests and branching
---------------------------
Issue pull requests for master. If it is a bug fix that also applies
for the latest release, it can be back ported to the stable branch
after merge.
Use one topic branch per pull request. If you do that, you can add
extra commits or fix up buggy commits via `git rebase -i`, and update
the branch. The updated branch will be visible in the same pull
request. Therefore, you should not open a new pull request when you
have to fix your changes.
Do not commit to master in your fork (for your own good).
Provide a clean branch without merge commits.
Committing your changes
-----------------------
Please ensure that all commits pass all tests, and do not have extra Dialyzer warnings.
To do that run `make check`.
#### Structuring your commits
Fixing a bug is one commit.
Adding a feature is one commit.
Adding two features is two commits.
Two unrelated changes is two commits.
If you fix a (buggy) commit, squash (`git rebase -i`) the changes as a fixup commit into
the original commit.
#### Writing Commit Messages
It's important to write a proper commit title and description. The commit title must be
at most 50 characters; it is the first line of the commit text. The second line of the
commit text must be left blank. The third line and beyond is the commit message. You
should write a commit message. If you do, wrap all lines at 72 characters. You should
explain what the commit does, what references you used, and any other information
that helps understanding your changes.
Basically, structure your commit message like this:
One line summary (at most 50 characters)
Longer description (wrap at 72 characters)
##### Commit title/summary
* At most 50 characters
* What was changed
* Imperative present tense (Fix, Add, Change)
* `Fix bug 123`
* `Add 'foobar' command`
* `Change default timeout to 123`
* No period
##### Commit description
* Wrap at 72 characters
* Why, explain intention and implementation approach
* Present tense
erlydtl-0.15.0/LICENSE 0000664 0000000 0000000 00000002143 15047124314 0014262 0 ustar 00root root 0000000 0000000 The MIT License
Copyright (c) 2008 Roberto Saccon, Evan Miller
Copyright (c) 2014 Andreas Stenius
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.
erlydtl-0.15.0/Makefile 0000664 0000000 0000000 00000002422 15047124314 0014715 0 ustar 00root root 0000000 0000000 REBAR=rebar3 $(REBAR_ARGS)
.PHONY: all
all: compile
.PHONY: compile
compile: slex
@$(REBAR) compile
.PHONY: slex
slex: src/erlydtl_scanner.erl
src/erlydtl_scanner.erl: src/erlydtl_scanner.slex
@$(REBAR) slex compile
.PHONY: check
check: test dialyze
.PHONY: test
test:
@$(REBAR) eunit
.PHONY: dialyze
dialyze: compile
@$(REBAR) dialyzer
.PHONY: clean
clean:
@$(REBAR) clean
.PHONY: realclean
realclean: clean
rm -rf _build
.PHONY: committed
committed:
@git diff --no-ext-diff --quiet --exit-code || { echo "there are uncommitted changes in the repo." ; false ;}
.PHONY: committed
release: committed check
@{ \
V0=$$(grep vsn src/erlydtl.app.src | sed -e 's/.*vsn,.*"\(.*\)".*/\1/') && \
V1=$$(grep '##' -m 1 NEWS.md | sed -e 's/##[^0-9]*\([0-9.-]*\).*/\1/') && \
read -e -p "OK, all tests passed, current version is $$V0, which version should we release now? ($$V1)" V2 && \
: $${V2:=$$V1} && \
echo "$$V2 it is..." && \
sed -i -e 's/vsn,.*}/vsn, "'$$V2'"}/' src/erlydtl.app.src && \
git ci -m "release v$$V2" src/erlydtl.app.src && \
git tag $$V2 && \
echo 'Updated src/erlydtl.app.src and tagged, run `git push origin master --tags` when ready' \
;}
erlydtl-0.15.0/NEWS.md 0000664 0000000 0000000 00000016641 15047124314 0014363 0 ustar 00root root 0000000 0000000 # ErlyDTL NEWS file
This file records noteworthy changes and additions to erlydtl as
suggested by the [GNU Coding
Standards](http://www.gnu.org/prep/standards/html_node/NEWS-File.html#NEWS-File).
## 0.14.0 (2021-03-19)
* #270 Added nowarn flag to export all in erlydtl_runtime
* #271 Hex wrong vsn and support OTP 23
## 0.13.0 (2020-02-15)
* #254 Semantical issue in rebar.config.script
* #255 OTP20 compatibility
* #261 OTP21 compatibility
* #263 Ensure modules are loaded when checking function_exported
* #267 Fix erlang:get_stacktrace() warning
## 0.12.1 (2016-09-21)
* #248 support filters in `{{ _(var) }}`.
## 0.12.0 (2016-08-08)
* #230 Use phash2/1 explicitly. It's not in compat layer
* #240 custom tags parameters parsing not handling newlines
* #242 Support for 19
* #246 make the 'join' filter work with numbers
## 0.11.1 (2015-10-26)
Fix build issues and missed update version info in .app-file.
## 0.11.0 (2015-10-25)
I've failed to keep track. Please check resolved issues on [github](https://github.com/erlydtl/erlydtl).
## 0.10.0 (2014-12-20)
* Fix issue with generated code for `for` loops (#167).
* Fix issue with using keywords as attributes (#177), or as variables (#194).
* Fix issue when including multiple templates extending a common base template (#176).
* New `w` option for toggling compile time warnings. Currently, there is only one, `non_block_tag`,
which is triggered on any non-block data in an extends-template.
* Add missing features to the `cycle` tag (#195) (still missing
independently stepping the cycle value).
* Support records in regroup tag (#191).
* Support for maps (#196).
## 0.9.4 (2014-04-15)
* Fix compile time variables and constants (#61)
* The `vars` compile time option has been deprecated in favor of
`default_vars`.
* Support for translation contexts (#131)
`context` is now a reserved keyword.
* Support for plural forms in `blocktrans` blocks (#131)
As a side effect of the this, `count` and `plural` are now reserved
keywords (the latter only as the tag name).
* Renamed compile options for `translation_fun` and `locales` to align
with the render options counter parts.
* Support `_` prefix on template variables to avoid unused variable
warnings, Erlang style (#164).
* Switched to `eunit_formatters` by @seancribbs for improved eunit reporting.
* All tests pass on Erlang 17.0! :)
## 0.9.3 (2014-03-27)
* Fix release process to work for non-git installations (#154).
* Fix list indexing (#155).
* New option for 0-based list/tuple access (#156) (see README for details).
## 0.9.2 (2014-03-22)
* Added NEWS file.
* Fixed broken `compile_dir` (#146).
* The backwards incompatible change in 0.9.1 for directory-compiled
templates has been reverted. A new `render` function has been added
instead (#148).
* Fixed broken escape feature (#150).
* Added translator comments (#152, #127) (@seriyps).
## 0.9.1 (2014-03-02)
This release brings a row of major internal improvements and also a
few new and improved features.
* Replaced most of `erl_syntax` use for merl (#123).
* Refactored compiler and test suite (#134, #136).
* Updated result value for directory-compiled (`compile_dir`)
templates to return `{ok, Rendered} | {error, Reason}` just as for
normally compiled templates. Please note that this is a **backwards
incompatible change**.
* Regroup tag is now Django compatible! (the formerly required
`endregroup` tag has been made optional) (#101).
* The `trans` tag now supports the `{% trans "text string" as Varname
%}` construct to catch the translated text rather than output it in
place.
* `{{ block.super }}` is now supported in child blocks (#18).
* `Template:variables()` now only lists referenced render variables
(#118).
* New `erlydtl_library` behaviour for adding custom filters and tags
(#137).
* New `load` tag added. Only difference to that of django is that we
don't support loading a library from a package, as there are no
notion of packages in Erlang (#100).
* New compile options supporting the load tag: `libraries` and
`default_libraries`. See the readme. (the previous
`custom_tags_modules` and `custom_filters_modules` are still
supported, although being marked deprecated in favour of the new
options).
* New compile options for aid in debugging erlydtl itself:
`debug_compiler` and `debug_root` as described in the readme (#139).
See milestone 0.9.1 for a complete list of closed issues in this release.
Happy templating!
## 0.9.0 (2014-02-17)
After some fluctuating stability around *0.8.1*, this release brings a
set of improvements that has been tested not only by the test suite,
but also in a couple of projects.
* API changes. Added `_file` and `_template` suffixes to the compile
functions to explicitly convey what is to be compiled.
* Autoescape is now enabled by default. Use the relatively new
`auto_escape` option if you want this disabled by default.
* `blocktrans` blocks can now be translated to multiple locales in
runtime. (thanks to @seriyps).
* `trans` and `_(...)` tags can now be translated to multiple locales
at compile time. (thanks to @seriyps).
* Don't want to save the compiled .beam file? Use `{out_dir, false}`
compile time option (this was possible before as well, when no
out_dir was specified, but that results in a warning).
* The error reporting has been fixed for certain cases.
## 0.8.2 (2014-01-29)
My sincere apologies, this really should have been in the *0.8.1*
release.
This release brings the new error/warning options and return values
also to the `compile_dir` api function.
**Backwards incompatible notes:**
Please notice that the new error reporting options also introduces
subtle incompatible changes in the API (see #126).
It's not hard to adapt, but I want you all to be aware of this when
upgrading.
These changes were made already in *0.8.1*, but the specs were not, so
dialyzer did not pick up on them.
## 0.8.1 (2014-01-19)
This release is a clean-up release with a bunch of minor issues squeezed out.
* Improved options for error reporting.
* new auto_escape compile option for django purists.
* Cleaned out some warnings given by Dialyzer.
* Major improvements to the generated code for `for`-loops.
**Warning**
The `compile_dir` function is not updated to the new way of reporting
errors and warnings, and is in a semi-workable state. A fix is
committed to *master*, and will be included in the next release.
## 0.8.0 (2014-01-10)
This is a release to bring in all the changes together since *0.7.0*,
and will be the starting point for a lot of future improvements.
## 0.7.0 (2011-03-21
* Support no-argument version of `date` filter.
## 0.6.0 (2010-04-28)
* Implement `trans` tag with support for .po files Includes a .po
parser and generator, docs, and tests. See README_I18N.
Many thanks to David Garcia.
## 0.5.3 (2009-08-13)
* Create "ebin" directory if it does not exist. Thanks to uri.sharf
for the bug report, ja...@nialscorva.net for the patch.
## 0.5.2 (2009-02-13)
* Do not require a space between a variable name and closing bracket,
e.g. allow `{{var1}}`.
## 0.5.1 (2008-10-23)
* Applied patch from Dan Milstein to fix a crash when rendering floats.
* Added a baseline failing test case for the problem.
## 0.5.0 (2008-08-07)
* Run functional tests from "make test".
* Clean up the output from all test suites.
* New README file.
* Remove create_parser stuff because "make" handles this.
* Make `erlydtl:compile/2,3` a wrapper for `erlydtl_compiler:compile/2,3`.
erlydtl-0.15.0/README.markdown 0000664 0000000 0000000 00000050330 15047124314 0015757 0 ustar 00root root 0000000 0000000 ErlyDTL [](https://travis-ci.org/erlydtl/erlydtl)
=======
ErlyDTL compiles Django Template Language to Erlang bytecode.
Project homepage:
ErlyDTL implements the Django Template Language as documented for
version *1.6*, here:
Despite our best efforts to be completely compatible with the Django
Template Language, there are still a few
[differences](https://github.com/erlydtl/erlydtl#differences-from-standard-django-template-language).
Call for maintainers
====================
My current position in life does not give me the time I feel this project deserves. So, if you feel that there are things that ought to be done, and have the skills and time to do them, please get in touch. -- @kaos
### The erlydtl branches & tags
We follow a
[gitflow](http://nvie.com/posts/a-successful-git-branching-model/)
inspired branching model, where `master` is the latest released
version (a.k.a. stable), and use `develop` for stuff that may break
from time to time.
#### master branch
Releases are made from the *master* branch, with dependency versions
pinned down and a hard coded version number in the app file.
#### develop branch
This is were all the action is, and at times may be slightly
broken. Suitable for early adopters who aren't afraid of a little
debugging and hopefully also reporting issues.
#### release tags
Whenever *master* is deemed stable with a nice set of
additions/changes, it is tagged for a new release.
As we're still going for the big 1.0 release, breaking changes **may**
be introduced also on minor release bumps; but after that, we'll stick
with [semver](http://semver.org/).
Compilation
-----------
To compile ErlyDTL, run
make
in this directory.
#### Do not use Erlang R16B03
The erl syntax tools is broken in Erlang R16B03, use R16B03-1 or any
other supported version instead.
#### Do use a recent version of rebar
In case of compilation issues, make sure your version of rebar is up-to-date.
Older versions of rebar does not support the `raw` option for dependencies, and may produce an error
message like this:
```
ERROR: Invalid dependency specification {merl,".*",
{git,
"git://github.com/erlydtl/merl.git",
"28e5b3829168199e8475fa91b997e0c03b90d280"},
[raw]}
```
Template compilation
--------------------
Usage:
```erlang
erlydtl:compile_file("/path/to/template.dtl", my_module_name)
erlydtl:compile_file("/path/to/template.dtl", my_module_name, Options)
erlydtl:compile_template("{{ foo }}", my_module_name)
erlydtl:compile_template("{{ foo }}", my_module_name, Options)
```
Result:
```erlang
{ok, Module}
{ok, Module, Warnings}
{ok, Module, Binary}
{ok, Module, Binary, Warnings}
error
{error, Errors, Warnings}
```
Options is a proplist possibly containing:
* `auto_escape` - Control automatic HTML escaping of template
values. Enabled by default.
* `binary` - Include the compiled template binary code in the result
tuple (between the module name and any warning/error lists). Note,
this option is named the same as for the Erlang compiler, with
similar use, except that this option does NOT affect whether or not
a .beam file is saved.
* `binary_strings` - Whether to compile strings as binary terms
(rather than lists). Defaults to `true`.
* `compiler_options` - Proplist with extra options passed directly to
`compiler:forms/2`. This can prove useful when using extensions to
add extra defines etc when compiling the generated code.
* `constants` - Replace template variables with a constant value when
compiling the template. This can _not_ be overridden when rendering
the template. See also `default_vars`.
* `custom_filters_modules` **deprecated** - A list of modules to be
used for handling custom filters. The modules will be searched in
order and take precedence over the built-in filters. Each custom
filter should correspond to an exported filter, e.g.
```erlang
some_filter(Value) -> iolist()
```
If the filter takes any arguments (e.g. "foo:2"), those will be
added to the call:
```erlang
some_filter(Value, Arg) -> iolist()
```
* `custom_tags_dir` - Directory of DTL files (no extension) includable
as tags. E.g. if `$custom_tags_dir/foo` contains `{{ bar
}}`, then `{% foo bar=100 %}` will evaluate to `100`.
* `custom_tags_modules` **deprecated** - A list of modules to be used
for handling custom tags. The modules will be searched in order and
take precedence over `custom_tags_dir`. Each custom tag should
correspond to an exported function with one of the following
signatures:
```erlang
some_tag(TagVars) -> iolist()
some_tag(TagVars, Options) -> iolist()
```
The `TagVars` are variables provided to a custom tag in the
template's body (e.g. `{% foo bar=100 %}` results in `TagVars =
[{bar, 100}]`). The `Options` are options passed as the second
argument to the `render/2` call at render-time. (These may include
any options, not just `locale` and `translation_fun`.)
* `debug_compiler` - Enable compiler debug diagnostics. Currently it
debug prints the options passed to `compile:forms` (i.e. if
verbosity is >= 2; that is, with two or more `verbose` options) and
enables the saving of the compiled template in source form to a .erl
file.
* `debug_info` - This option is passed to `compile:forms` to include
debug information in the compiled module.
* `debug_root` - Only applies when `debug_compiler` is `true`. The
root directory for debug source dumps. If set to `false`, no source
dump will be saved. Defaults to `undefined`, leaving the source dump
next to the source template file.
* `default_libraries` - A list of libraries that should be loaded by
default when compiling a template. Libraries can be specified either
by name (when there is a name to module mapping also provided in the
`libraries` option) or by module.
* `default_vars` - Provide default values for variables. Any value
from the render variables takes precedence. Notice: in case the
value is a `fun/0`, it will be called at compile time. See also
`constants`.
* `doc_root` - Included template paths will be relative to this
directory; defaults to the compiled template's directory.
* `extension_module` **experimental** - This is work in progress to
make erlydtl extensible.
* `force_recompile` - Recompile the module even if the source's
checksum has not changed. Useful for debugging.
* `libraries` - A list of `{Name, Module}` libraries implementing
custom tags and filters. `Module` should implement the
`erlydtl_library` behaviour (see [Custom tags and filters] below).
* `lists_0_based` - **Compatibility warning** Defaults to `false`,
giving 1-based list access, as is common practice in Erlang. Set it
to `true` to get 1-based access as in Django, or to `defer` to not
decide until render time, using the render option
`lists_0_based`. See also `tuples_0_based`.
* `locale` - Locale to translate to during compile time. May be
specified multiple times as well as together with the `locales`
option.
* `locales` - A list of locales to be passed to `translation_fun`.
Defaults to [].
* `no_env` - Do not read additional options from the OS environment
variable `ERLYDTL_COMPILER_OPTIONS`.
* `no_load` - Do not load the compiled template.
* `out_dir` - Directory to store generated .beam files. If not
specified, no .beam files will be created and a warning is
emitted. To silence the warning, use `{out_dir, false}`.
* `reader` - {module, function} tuple that takes a path to a template,
may be ReaderOptions and returns a binary with the file contents.
Defaults to `{file, read_file}`. Useful for reading templates from
a network resource.
* `reader_options` - list of {option_name, Option} that passed as the
second parameter to `reader`.
```erlang
extra_reader(FileName, ReaderOptions) ->
UserID = proplists:get_value(user_id, ReaderOptions, <<"IDUnknown">>),
UserName = proplists:get_value(user_name, ReaderOptions, <<"NameUnknown">>),
case file:read_file(FileName) of
{ok, Data} when UserID == <<"007">>, UserName == <<"Agent">> ->
{ok, Data};
{ok, _Data} ->
{error, "Not Found"};
Err ->
Err
end.
```
```
CompileResult = erlydtl:compile(Body, ModName,
[return,
{reader, {?MODULE, extra_reader}},
{reader_options, [{user_id, <<"007">>},
{user_name, <<"Agent">>}]}
]),
```
* `record_info` - List of records to look for when rendering the
template. Each record info is a tuple with the fields of the record:
```erlang
{my_record, record_info(fields, my_record)}
```
* `return` - Short form for both `return_warnings` and `return_errors`.
* `return_warnings` - If this flag is set, then an extra field
containing warnings is added to the tuple returned on success.
* `return_errors` - If this flag is set, then an error-tuple with two
extra fields containing errors and warnings is returned when there
are errors.
* `report` - Short form for both `report_warnings` and `report_errors`.
* `report_warnings` - Print warnings as they occur.
* `report_errors` - Print errors as they occur.
* `translation_fun` - A two-argument fun to use for translating
`blocktrans` blocks, `trans` tags and `_(..)` expressions at compile
time. This will be called once for each pair of translated element
and locale specified with `locales` and `locale` options. The fun
should take the form:
```erlang
fun (Block::string(), Locale|{Locale, Context}) ->
<<"ErlyDTL code">>::binary() | default
when Locale::string(), Context::string().
```
Please, keep in mind, that if your templates where not specially
designed, you probably still need render time translations.
See description of the `translation_fun` render option for more
details on the translation `context`.
Notice, you may instead pass a `fun/0`, `{Module, Function}` or
`{Module, Function, Args}` which will be called recursively until it
yields a valid translation function, at which time any needed
translation setup actions can be carried out prior to returning the
next step (either another setup function/tuple, or the translation
function).
```erlang
%% sample translation setup
fun () ->
translation_engine:init(),
fun translation_engine:translate/2
end
```
* `tuples_0_based` - **Compatibility warning** Defaults to `false`,
giving 1-based tuple access, as is common practice in Erlang. Set it
to `true` to get 1-based access as in Django, or to `defer` to not
decide until render time, using the render option
`tuples_0_based`. See also `lists_0_based`.
* `vars` **deprecated** - Use `default_vars` instead. Variables (and
their values) to evaluate at compile-time rather than
render-time.
* `verbose` - Enable verbose printing of compilation progress. Add
several for even more verbose (e.g. debug) output.
* `w` - Enable/Disable compile time checks.
Available checks:
- `non_block_tag` indicated that there is other data than `block` tags in an extends-template
(e.g. a template that begins with the `extends` tag).
* `warnings_as_errors` - Treat warnings as errors.
Helper compilation
------------------
Helpers provide additional templating functionality and can be used in
conjunction with the `custom_tags_module` option above. They can be
created from a directory of templates thusly:
```erlang
erlydtl:compile_dir("/path/to/dir", my_helper_module_name)
erlydtl:compile_dir("/path/to/dir", my_helper_module_name, Options)
```
The resulting module will export a function for each template
appearing in the specified directory. Options is the same as for
`compile/3`.
Compiling a helper module can be more efficient than using
`custom_tags_dir` because the helper functions will be compiled only
once (rather than once per template).
Notice: The exported template functions return an `iolist()` on
success only, failures are non-local (e.g. as a throw). To get the
result in wrapped tuple `{ok, iolist()} | {error, Reason}` call one of
the `render` functions: `render(Tag) | render(Tag, Vars) | render(Tag,
Vars, Opts)`.
Usage (of a compiled template)
------------------------------
### render/1
```erlang
my_compiled_template:render(Variables) -> {ok, IOList} | {error, Err}
```
Variables is a proplist, dict, gb_tree, or a parameterized module
(whose method names correspond to variable names). The variable values
can be atoms, strings, binaries, or (nested) variables.
IOList is the rendered template.
### render/2
```erlang
my_compiled_template:render(Variables, Options) -> {ok, IOList} | {error, Err}
```
Same as `render/1`, but with the following options:
* `translation_fun` - A `fun/1` or `fun/2` that will be used to
translate strings appearing inside `{% trans %}` and `{% blocktrans
%}` tags at render-time. The simplest TranslationFun would be
`fun(Val) -> Val end`. Placeholders for blocktrans variable
interpolation should be wrapped in `{{` and `}}`. In case of
`fun/2`, the extra argument is the current locale, possibly together
with a translation context in a tuple:
```erlang
fun (Val|{Val, {Plural_Val, Count}}, Locale|{Locale, Context}) ->
Translated_Val
end
```
The context is present when specified in the translation
tag. Example:
```django
{% trans "Some text to translate" context "app-specific" %}
or
{% blocktrans context "another-context" %}
Translate this for {{ name }}.
{% endblocktrans %}
```
The plural form is present when using `count` and `plural` in a
`blocktrans` block:
```django
{% blocktrans count counter=var|length %}
There is {{ counter }} element in the list.
{% plural %}
There are {{ counter }} elements in the list.
{% endblocktrans %}
```
Render time translation function is also used to translate dates.
Date tokens mimics those used in django, so you may reuse django translations.
Tokens may appear in a date are:
1. Full months names, capitalized ("January" .. "December");
2. 3 letters months names ("jan" .. "dec");
3. Associated Press style months
("Jan." .. "Dec." with "abbrev.month" context);
4. Alternative month name, for "E" option
("January" .. "December" with "alt. month" context);
5. Full week day names, capitalized ("Monday" .. "Sunday");
6. Abbreviated week day names, capitalized ("Mon" .. "Sun");
7. day time tokens ("AM", "PM", "a.m.", "p.m.", "noon", "midnight");
While date token values may be passed as lists, consider using binaries
as a default string format.
Here is a simple but robust translation function stub:
```erlang
translation_placeholder({Val,{Plural, Count}}, {L, C}) when is_list(Val) ->
translation_placeholder({list_to_binary(Val),{Plural, Count}}, LC);
translation_placeholder(Val, {L, C}) when is_list(Val) ->
translation_placeholder(list_to_binary(Val), LC);
translation_placeholder(Val, {L, C}) when is_list(C) ->
translation_placeholder(Val, list_to_binary(C));
translation_placeholder(Val, {L, C}) when is_list(C) ->
io:format("Translating ~p into ~p with context ~p~n", [Val, L, C]),
%% do nothing and return original value
Val.
```
The translation fun can also be a `fun/0`, `{Module, Function}` or
`{Module, Function, Args}` which will be called recursively until it
yields a valid translation function, at which time any needed
translation setup actions can be carried out prior to returning the
next step (either another setup function/tuple, or the translation
function).
* `lists_0_based` - If the compile option `lists_0_based` was set to
`defer`, pass this option (or set it to true, `{lists_0_based,
true}`) to get 0-based list indexing when rendering the
template. See also `tuples_0_based`.
* `locale` - A string specifying the current locale, for use with the
`translation_fun` compile-time option.
* `tuples_0_based` - If the compile option `tuples_0_based` was set to
`defer`, pass this option (or set it to true, `{tuples_0_based,
true}`) to get 0-based tuple indexing when rendering the
template. See also `lists_0_based`.
### translatable_strings/0
```erlang
my_compiled_template:translatable_strings() -> [String]
```
List of strings appearing in `{% trans %}` and `_(..)` tags.
### translated_blocks/0
```erlang
my_compiled_template:translated_blocks() -> [String]
```
List of strings appearing in `{% blocktrans %}...{% endblocktrans %}`
blocks; the translations (which can contain ErlyDTL code) are
hard-coded into the module and appear at render-time. To get a list of
translatable blocks before compile-time, use the provided
`blocktrans_extractor` module.
### source/0
```erlang
my_compiled_template:source() -> {FileName, CheckSum}
```
Name and checksum of the original template file.
### dependencies/0
```erlang
my_compiled_template:dependencies() -> [{FileName, CheckSum}]
```
List of names/checksums of templates included by the original template
file. Useful for frameworks that recompile a template only when the
template's dependencies change.
### variables/0
```erlang
my_compiled_template:variables() -> [Variable::atom()]
```
Sorted list of unique variables used in the template's body. The list
can be used for determining which variable bindings need to be passed
to the `render/3` function.
### default_variables/0
```erlang
my_compiled_template:default_variables() -> [Variable::atom()]
```
Like `variables/0`, but for any variable which have a default value
provided at compile time.
### constants/0
```erlang
my_compiled_template:constants() -> [Variable::atom()]
```
Like `default_variables/0`, but these template variables has been
replaced with a fixed value at compile time and can not be changed
when rendering the template.
Custom tags and filters
-----------------------
Starting with release *0.9.1*, the recommended way to add custom tags
and filters are to register a module implementing the
`erlydtl_library` behaviour. There are two functions needed to
implement a custom library: `version/0` and `inventory/1`.
The `version/0` function is to be able to keep backwards compatibility
in face of an evolving library behaviour, and should return the
version of the behaviour the library supports. The valid range of
versions is in the spec for the `version/0` callback in
`erlydtl_library.erl`.
### Library version 1
The `inventory/1` function is called with either `filters` or `tags`
as argument, and should return a list of all filters or tags available
in that library, respectively (see spec in `erlydtl_library.erl` for
details on syntax for this list).
Tag functions must take a list of arguments, and may take a list of
render options, and should return an `iolist()` as result value (see
`custom_tags_modules` compile option).
Filter functions must take an `iolist()` value to filter, and may take
an argument, and should return an `iolist()` as result value (see
`custom_filters_modules` compile option).
Differences from standard Django Template Language
--------------------------------------------------
* `csrf_token` The
[Cross Site Request Forgery](https://docs.djangoproject.com/en/1.6/ref/contrib/csrf/)
tag is not implemented.
* `url` The
[url](https://docs.djangoproject.com/en/1.6/ref/templates/builtins/#url)
tag is not implemented. This should be
[addressed](https://github.com/erlydtl/erlydtl/issues/115) in a
future release.
* List indexing is 1-based in erlydtl, while 0-based in Django (see
[#156](https://github.com/erlydtl/erlydtl/issues/156)).
* For an up-to-date list, see all
[issues](https://github.com/erlydtl/erlydtl/issues) marked
`dtl_compat`.
* Erlang specifics: Template variables may be prefixed with underscore
(`_`) to avoid "unused variable" warnings (see
[#164](https://github.com/erlydtl/erlydtl/issues/164)).
* `cycle` tags do not support independently moving the cycle value
from the original loop.
Tests
-----
From a Unix shell, run:
make tests
Note that the tests will create some output in tests/output in case of regressions.
License
-------
ErlyDTL is released under the MIT license.
erlydtl-0.15.0/README_I18N 0000664 0000000 0000000 00000006270 15047124314 0014701 0 ustar 00root root 0000000 0000000 Generate gettext infrastructure
-------------------------------
Erlydtl allows templates to use i18n features based on gettext. Standard po
files can be used to generate i18ized templates. A template parser/po generator
is also provided.
Translation done by `{%trans%}`, `{%blocktrans%}` tags and by wrapping variables
in `_(...)` construction.
Translation may be applied in compile time (translated strings are embedded in
compiled template code) or in runtime (template will query gettext server during
template rendering). 1'st is faster in terms of template rendering time, 2'nd is
more flexible (you may update locales without any template recompilation).
In order to apply compile-time translation, you must pass `blocktrans_fun` plus
`blocktrans_locales` (this will translate `{%blocktrans%}`) and/or `locale` (this
translates `{%trans%}` and `_("string literal")` values) to `erlydtl:compile/3`.
Next, you should pass `locale` option to `my_compiled_template:render/2`.
If you prefer runtime translation, just don't pass `blocktrans_fun` and/or `locale`
compilation options and add `translation_fun` option to `my_compiled_template:render/2`.
1. In order to enable i18n you first, you'll need gettext library to be
available on your lib_path.
Library can be downloaded from http://github.com/etnt/gettext
2. Then you'll need to add a parse target on your makefile (or the script
used to trigger template reparsing) trans:
erl -pa ./ebin ./deps/*/ebin -noshell -s reloader -run i18n_manager \
generate_pos "en,es" "./views/*/*.html,./views/*.html"
rm -rf $(GETTEXT_DIR)/lang/default-old
mv $(GETTEXT_DIR)/lang/default $(GETTEXT_DIR)/lang/default-old
cp -rf $(GETTEXT_DIR)/lang/$(GETTEXT_TMP_NAME) $(GETTEXT_DIR)/lang/default
rm -rf $(GETTEXT_DIR)/lang/$(GETTEXT_TMP_NAME)/*
Mind that GETTEXT_DIR and GETTEXT_TMP_NAME must be bound to existing
directories. Args passed to i18n_manager:generate_pos are locales that
will be supported (generating dir structure and po files) and
directories where generator will search for template files including
trans tags.
3. If you wish to translate templates at compile-time, gettext server must be
running before template parsing and it must be
populated with the content of the po files. Consider adding this
snipplet to the code before template parsing
gettext_server:start(),
LoadPo =
fun(Lang)->
{_, Bin} = file:read_file("./lang/default/"++ Lang ++"/gettext.po"),
gettext:store_pofile(Lang, Bin)
end,
lists:map(LoadPo, ["es","en"]).
Here locales are the codes are provided to gettext. Those codes must be
a subset of the locales provided to po generation process.
4. Update strings. Edit po files on $(GETTEXT_DIR)/lang/default/$(LOCALE)/gettext.po
translating msgstr to the translated version of their corresponding msgstr.
5. Generate localized templates providing `blocktrans_fun` and
`blocktrans_locales` compile options or use runtime
translation via `translation_fun` rendering option.
erlydtl-0.15.0/bin/ 0000775 0000000 0000000 00000000000 15047124314 0014025 5 ustar 00root root 0000000 0000000 erlydtl-0.15.0/bin/erlydtl_compile 0000775 0000000 0000000 00000001217 15047124314 0017143 0 ustar 00root root 0000000 0000000 #!/usr/bin/env escript
%% -*- mode: erlang -*-
-export([main/1]).
%% External API
main([Prefix, File]) ->
ensure(),
Basename = filename:basename(File, ".dtl"),
ModuleName = list_to_atom(string:to_lower(lists:flatten([Prefix, "_", Basename]))),
erlydtl_compiler:compile(File, ModuleName, [{out_dir, filename:dirname(File)}]);
main(_) ->
usage().
ensure() ->
code:add_patha("ebin"),
code:add_patha("lib/erlydtl/ebin"),
[D1,D2|_] = code:get_path(),
io:format("Code Path: ~p~n", [[D1,D2]]).
usage() ->
io:format("usage: ~s name [destdir]~n",
[filename:basename(escript:script_name())]),
halt(1).
erlydtl-0.15.0/include/ 0000775 0000000 0000000 00000000000 15047124314 0014700 5 ustar 00root root 0000000 0000000 erlydtl-0.15.0/include/erlydtl_ext.hrl 0000775 0000000 0000000 00000004474 15047124314 0017762 0 ustar 00root root 0000000 0000000
-record(error_info, {
return = false,
report = false,
list = []
}).
-record(dtl_context, {
local_scopes = [],
block_dict = dict:new(),
trans_fun = none,
trans_locales = [],
auto_escape = [off],
doc_root = "",
parse_trail = [],
vars = [],
const = [],
record_info = [],
filters = [],
tags = [],
libraries = [],
custom_tags_dir = [],
reader = {file, read_file},
reader_options = [],
module = undefined,
compiler_options = [],
binary_strings = true,
force_recompile = false,
verbose = 0,
is_compiling_dir = false,
extension_module = undefined,
scanner_module = erlydtl_scanner,
scanned_tokens = [],
all_options = [],
errors = #error_info{},
warnings = #error_info{},
bin = undefined,
lists_0_based = false,
tuples_0_based = false,
checks = [non_block_tag]
}).
%% ALL fields of ast_info{} must be lists (see erlydtl_compiler_utils:merge_info/2)
-record(ast_info, {
dependencies = [],
translatable_strings = [],
translated_blocks= [],
custom_tags = [],
var_names = [],
def_names = [],
const_names = []
}).
-record(treewalker, {
counter = 0,
safe = false,
extension = undefined,
context
}).
-record(scanner_state, {
template=[],
scanned=[],
pos={1,1},
state=in_text
}).
-define(ERR(Err, Ctx), erlydtl_compiler_utils:add_error(?MODULE, Err, Ctx)).
-define(WARN(Warn, Ctx), erlydtl_compiler_utils:add_warning(?MODULE, Warn, Ctx)).
-define(V_INFO,1).
-define(V_DEBUG,2).
-define(V_TRACE,3).
-define(LOG_INFO(Fmt, Args, Ctx), erlydtl_compiler_utils:print(?V_INFO, Fmt, Args, Ctx)).
-define(LOG_DEBUG(Fmt, Args, Ctx), erlydtl_compiler_utils:print(?V_DEBUG, Fmt, Args, Ctx)).
-define(LOG_TRACE(Fmt, Args, Ctx), erlydtl_compiler_utils:print(?V_TRACE, Fmt, Args, Ctx)).
-ifndef(OTP_RELEASE).
-define(WITH_STACKTRACE(T, R, S), T:R -> S = erlang:get_stacktrace(), ).
-else.
-define(WITH_STACKTRACE(T, R, S), T:R:S ->).
-endif.
erlydtl-0.15.0/include/erlydtl_preparser.hrl 0000775 0000000 0000000 00000016151 15047124314 0021160 0 ustar 00root root 0000000 0000000 %% -*- mode: erlang -*-
%% vim: syntax=erlang
%% This file is based on parsetools/include/yeccpre.hrl
%%
%% The applied modifiactions are to enable the caller to recover
%% after a parse error, and then resume normal parsing.
%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1996-2015. All Rights Reserved.
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% %CopyrightEnd%
%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% The parser generator will insert appropriate declarations before this line.%
-export([parse/1, parse_and_scan/1, format_error/1, resume/1]).
-include("erlydtl_ext.hrl").
-type yecc_ret() :: {'error', _, _} | {'ok', _}.
-spec parse(Tokens :: list()) -> yecc_ret().
parse(Tokens) ->
yeccpars0(Tokens, {no_func, no_line}, 0, [], []).
-spec parse_and_scan({function() | {atom(), atom()}, [_]}
| {atom(), atom(), [_]}) -> yecc_ret().
parse_and_scan({F, A}) ->
yeccpars0([], {{F, A}, no_line}, 0, [], []);
parse_and_scan({M, F, A}) ->
Arity = length(A),
yeccpars0([], {{fun M:F/Arity, A}, no_line}, 0, [], []).
resume([Tokens, Tzr, State, States, Vstack]) ->
yeccpars0(Tokens, Tzr, State, States, Vstack).
-spec format_error(any()) -> [char() | list()].
format_error(Message) ->
case io_lib:deep_char_list(Message) of
true ->
Message;
_ ->
io_lib:write(Message)
end.
%% To be used in grammar files to throw an error message to the parser
%% toplevel. Doesn't have to be exported!
-compile({nowarn_unused_function, return_error/2}).
-spec return_error(integer(), any()) -> no_return().
return_error(Line, Message) ->
throw({error, {Line, ?MODULE, Message}}).
-compile({nowarn_unused_function, return_state/0}).
return_state() ->
throw(return_state).
-define(CODE_VERSION, "1.4").
yeccpars0(Tokens, Tzr, State, States, Vstack) ->
try yeccpars1(Tokens, Tzr, State, States, Vstack)
catch
?WITH_STACKTRACE(error, Error, Stacktrace)
try yecc_error_type(Error, Stacktrace) of
Desc ->
erlang:raise(error, {yecc_bug, ?CODE_VERSION, Desc},
Stacktrace)
catch _:_ -> erlang:raise(error, Error, Stacktrace)
end;
%% Probably thrown from return_error/2:
throw: {error, {_Line, ?MODULE, _M}} = Error ->
Error
end.
yecc_error_type(function_clause, [{?MODULE,F,ArityOrArgs,_} | _]) ->
case atom_to_list(F) of
"yeccgoto_" ++ SymbolL ->
{ok,[{atom,_,Symbol}],_} = erl_scan:string(SymbolL),
State = case ArityOrArgs of
[S,_,_,_,_,_,_] -> S;
_ -> state_is_unknown
end,
{Symbol, State, missing_in_goto_table}
end.
-define(checkparse(CALL, STATE),
try case CALL of
{error, Error} ->
{error, Error, STATE};
Else ->
Else
end
catch
throw: return_state ->
{ok, STATE}
end).
yeccpars1([Token | Tokens], Tzr, State, States, Vstack) ->
?checkparse(
yeccpars2(State, element(1, Token), States, Vstack, Token, Tokens, Tzr),
[[Token|Tokens], Tzr, State, States, Vstack]
);
yeccpars1([], {{F, A},_Line}, State, States, Vstack) ->
case apply(F, A) of
{ok, Tokens, Endline} ->
yeccpars1(Tokens, {{F, A}, Endline}, State, States, Vstack);
{eof, Endline} ->
yeccpars1([], {no_func, Endline}, State, States, Vstack);
{error, Descriptor, _Endline} ->
{error, Descriptor}
end;
yeccpars1([], {no_func, no_line}, State, States, Vstack) ->
Line = 999999,
yeccpars2(State, '$end', States, Vstack, yecc_end(Line), [],
{no_func, Line});
yeccpars1([], {no_func, Endline}, State, States, Vstack) ->
yeccpars2(State, '$end', States, Vstack, yecc_end(Endline), [],
{no_func, Endline}).
%% yeccpars1/7 is called from generated code.
%%
%% When using the {includefile, Includefile} option, make sure that
%% yeccpars1/7 can be found by parsing the file without following
%% include directives. yecc will otherwise assume that an old
%% yeccpre.hrl is included (one which defines yeccpars1/5).
yeccpars1(State1, State, States, Vstack, Token0, [Token | Tokens], Tzr) ->
?checkparse(
yeccpars2(State, element(1, Token), [State1 | States],
[Token0 | Vstack], Token, Tokens, Tzr),
[[Token0, Token | Tokens], Tzr, State1, States, Vstack]
);
yeccpars1(State1, State, States, Vstack, Token0, [], {{_F,_A}, _Line}=Tzr) ->
yeccpars1([], Tzr, State, [State1 | States], [Token0 | Vstack]);
yeccpars1(State1, State, States, Vstack, Token0, [], {no_func, no_line}) ->
Line = yecctoken_end_location(Token0),
yeccpars2(State, '$end', [State1 | States], [Token0 | Vstack],
yecc_end(Line), [], {no_func, Line});
yeccpars1(State1, State, States, Vstack, Token0, [], {no_func, Line}) ->
yeccpars2(State, '$end', [State1 | States], [Token0 | Vstack],
yecc_end(Line), [], {no_func, Line}).
%% For internal use only.
yecc_end({Line,_Column}) ->
{'$end', Line};
yecc_end(Line) ->
{'$end', Line}.
yecctoken_end_location(Token) ->
try erl_anno:end_location(element(2, Token)) of
undefined -> yecctoken_location(Token);
Loc -> Loc
catch _:_ -> yecctoken_location(Token)
end.
-compile({nowarn_unused_function, yeccerror/1}).
yeccerror(Token) ->
Text = yecctoken_to_string(Token),
Location = yecctoken_location(Token),
{error, {Location, ?MODULE, ["syntax error before: ", Text]}}.
-compile({nowarn_unused_function, yecctoken_to_string/1}).
yecctoken_to_string(Token) ->
try erl_scan:text(Token) of
undefined -> yecctoken2string(Token);
Txt -> Txt
catch _:_ -> yecctoken2string(Token)
end.
yecctoken_location(Token) ->
try erl_scan:location(Token)
catch _:_ -> element(2, Token)
end.
-compile({nowarn_unused_function, yecctoken2string/1}).
yecctoken2string({atom, _, A}) -> io_lib:write(A);
yecctoken2string({integer,_,N}) -> io_lib:write(N);
yecctoken2string({float,_,F}) -> io_lib:write(F);
yecctoken2string({char,_,C}) -> io_lib:write_char(C);
yecctoken2string({var,_,V}) -> io_lib:format("~s", [V]);
yecctoken2string({string,_,S}) -> io_lib:write_string(S);
yecctoken2string({reserved_symbol, _, A}) -> io_lib:write(A);
yecctoken2string({_Cat, _, Val}) -> io_lib:format("~p",[Val]);
yecctoken2string({dot, _}) -> "'.'";
yecctoken2string({'$end', _}) ->
[];
yecctoken2string({Other, _}) when is_atom(Other) ->
io_lib:write(Other);
yecctoken2string(Other) ->
io_lib:write(Other).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
erlydtl-0.15.0/priv/ 0000775 0000000 0000000 00000000000 15047124314 0014235 5 ustar 00root root 0000000 0000000 erlydtl-0.15.0/priv/custom_tags/ 0000775 0000000 0000000 00000000000 15047124314 0016565 5 ustar 00root root 0000000 0000000 erlydtl-0.15.0/priv/custom_tags/flashvideo 0000664 0000000 0000000 00000001734 15047124314 0020641 0 ustar 00root root 0000000 0000000
erlydtl-0.15.0/rebar-slex.config 0000664 0000000 0000000 00000000213 15047124314 0016504 0 ustar 00root root 0000000 0000000 %% -*- mode: erlang -*-
{deps,
[{slex, ".*", {git, "git://github.com/erlydtl/slex.git", {tag, "0.2.1"}}}
]
}.
{plugins, [rebar_slex]}.
erlydtl-0.15.0/rebar-tests.config 0000664 0000000 0000000 00000001152 15047124314 0016676 0 ustar 00root root 0000000 0000000 %% -*- mode: erlang -*-
{eunit_opts,
[%% This turns off the default output, MUST HAVE
no_tty,
%% Uses the progress formatter with ANSI-colored output
{report, {eunit_progress, [colored
%% uncomment to get a list of slowest running tests
%%, profile
]}}
]}.
{deps,
[{eunit_formatters, ".*",
{git, "git://github.com/seancribbs/eunit_formatters", "35e3e6ab2db48af776a1a21bba6f1461c97caacb"}}
]}.
{pre_hooks,
[{eunit,
"erlc -I include/erlydtl_preparser.hrl -o test"
" test/erlydtl_extension_testparser.yrl"}
]}.
erlydtl-0.15.0/rebar.config 0000664 0000000 0000000 00000001001 15047124314 0015527 0 ustar 00root root 0000000 0000000 %% -*- mode: erlang -*-
{require_otp_vsn, "18"}.
{erl_opts, [debug_info]}.
{yrl_opts, [{includefile, "include/erlydtl_preparser.hrl"}]}.
{project_plugins,
[
{slex, {git, "https://github.com/erlydtl/slex.git", {tag, "0.2.3"}}}
]}.
{profiles,
[{test,
[{pre_hooks,
[
{compile, "erlc -I include/erlydtl_preparser.hrl -o test test/erlydtl_extension_testparser.yrl"}
]}
]}
]}.
{dialyzer,
[
{warnings, [underspecs, unmatched_returns, race_conditions, error_handling]}
]}.
erlydtl-0.15.0/rebar.lock 0000664 0000000 0000000 00000000004 15047124314 0015214 0 ustar 00root root 0000000 0000000 [].
erlydtl-0.15.0/src/ 0000775 0000000 0000000 00000000000 15047124314 0014044 5 ustar 00root root 0000000 0000000 erlydtl-0.15.0/src/erlydtl.app.src 0000664 0000000 0000000 00000000476 15047124314 0017022 0 ustar 00root root 0000000 0000000 %% -*- mode: erlang -*-
{application, erlydtl,
[{description, "Django Template Language for Erlang"},
{vsn, "0.14.0"},
{modules, []},
{applications, [kernel, stdlib, compiler, syntax_tools]},
{registered, []},
{licenses,["MIT"]},
{links,[{"Github",
"https://github.com/erlydtl/erlydtl"}]}
]}.
erlydtl-0.15.0/src/erlydtl.erl 0000664 0000000 0000000 00000017747 15047124314 0016247 0 ustar 00root root 0000000 0000000 %%%-------------------------------------------------------------------
%%% File: erlydtl.erl
%%% @author Roberto Saccon [http://rsaccon.com]
%%% @author Evan Miller
%%% @author Andreas Stenius
%%% @copyright 2008 Roberto Saccon, Evan Miller
%%% @copyright 2014 Andreas Stenius
%%% @doc
%%% Public interface for ErlyDTL
%%% @end
%%%
%%% The MIT License
%%%
%%% Copyright (c) 2008 Roberto Saccon, Evan Miller
%%% Copyright (c) 2014 Andreas Stenius
%%%
%%% 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.
%%%
%%% @since 2007-11-11 by Roberto Saccon, Evan Miller
%%% @since 2014 by Andreas Stenius
%%%-------------------------------------------------------------------
-module(erlydtl).
-author('rsaccon@gmail.com').
-author('emmiller@gmail.com').
-author('Andreas Stenius ').
%% --------------------------------------------------------------------
%% API
%% --------------------------------------------------------------------
-export([compile_file/2, compile_file/3]).
-export([compile_template/2, compile_template/3]).
-export([compile/2, compile/3]).
-export([compile_dir/2, compile_dir/3]).
-type position() :: non_neg_integer().
-type location() :: none | position() | {Line::position(), Column::position()}.
-type error_info() :: {File::list(),
[{location(),
Module::atom(),
ErrorDesc::term()}]}.
-type errors() :: list(error_info()).
-type warnings() :: list(error_info()).
-type ok_ret() :: {ok, Module::atom()} | {ok, Module::atom(), warnings()}.
-type err_ret() :: error | {error, errors(), warnings()}.
-type filename() :: file:name_all().
-type compiler_options() :: compiler_option() | compile:option().
-type compiler_option() :: return | return_warnings | return_errors
| report | report_warnings | report_errors
| warnings_as_errors | debug_info | verbose.
-type compile_options() :: [compile_option() | {atom(), term()}].
-type compile_option() :: compiler_option()
| auto_escape | binary | binary_strings
| force_recompile | no_env | no_load
| {blocktrans_fun, Trans::fun((Block::iodata(), Locale::string()) ->
iodata() | default)}
| {blocktrans_locales, [string()]}
| {compiler_options, [compiler_options()]}
| {custom_filters_modules, [Module::atom]}
| {custom_tags_dirs, [filename()]}
| {custom_tags_modules, [Module::atom]}
| {default_libraries, [Name::atom()]}
| {doc_root, filename()}
| {extension_module, Module::atom()}
| {libraries, [{Name::atom(), Module::atom()}]}
| {locale, string()}
| {out_dir, false | filename()}
| {reader, {Module::atom(), Function::atom}}
| {reader_options, [{Name::atom(), iodata()}]}
| {record_info, [{Name::atom(), [Field::atom()]}]}
| {scanner_module, Module::atom()}
| {vars, [{atom(), iodata()}]}.
%% --------------------------------------------------------------------
%% Compile file
%% --------------------------------------------------------------------
-spec compile_file(filename(), atom()) -> {ok, Module::atom()} | error.
compile_file(File, Module) ->
erlydtl_compiler:compile_file(File, Module, erlydtl_compiler:default_options()).
-spec compile_file(filename(), atom(), compile_options()) -> ok_ret() | err_ret().
compile_file(File, Module, Options) ->
erlydtl_compiler:compile_file(File, Module, Options).
%% --------------------------------------------------------------------
%% Compile template
%% --------------------------------------------------------------------
-spec compile_template(iodata(), atom()) -> {ok, Module::atom()} | error.
compile_template(Template, Module) ->
erlydtl_compiler:compile_template(Template, Module, erlydtl_compiler:default_options()).
-spec compile_template(iodata(), atom(), compile_options()) -> ok_ret() | err_ret().
compile_template(Template, Module, Options) ->
erlydtl_compiler:compile_template(Template, Module, Options).
%% --------------------------------------------------------------------
%% Compile directory
%% --------------------------------------------------------------------
-spec compile_dir(filename(), atom()) -> {ok, Module::atom()} | error.
compile_dir(DirectoryPath, Module) ->
erlydtl_compiler:compile_dir(DirectoryPath, Module, erlydtl_compiler:default_options()).
-spec compile_dir(filename(), atom(), compile_options()) -> ok_ret() | err_ret().
compile_dir(DirectoryPath, Module, Options) ->
erlydtl_compiler:compile_dir(DirectoryPath, Module, Options).
%% --------------------------------------------------------------------
%% Legacy API
%% --------------------------------------------------------------------
%% keep for backwards compatibility, with a tuple-twist to ease migration / offer alternative path..
-spec compile(FileOrTemplate, atom()) -> {ok, Module::atom()} | error
when FileOrTemplate :: string() | binary()
| {file, filename()}
| {template, iodata()}
| {dir, filename()}.
compile({file, File}, Module) ->
compile_file(File, Module);
compile({template, Template}, Module) ->
compile_template(Template, Module);
compile({dir, Directory}, Module) ->
compile_dir(Directory, Module);
compile(FileOrTemplate, Module) when is_binary(FileOrTemplate) ->
compile_template(FileOrTemplate, Module);
compile(FileOrTemplate, Module) ->
compile_file(FileOrTemplate, Module).
-spec compile(FileOrTemplate, atom(), compile_options() ) -> ok_ret() | err_ret()
when FileOrTemplate :: string() | binary()
| {file, filename()}
| {template, iodata()}
| {dir, filename()}.
compile({file, File}, Module, Options) ->
compile_file(File, Module, Options);
compile({template, Template}, Module, Options) ->
compile_template(Template, Module, Options);
compile({dir, Directory}, Module, Options) ->
compile_dir(Directory, Module, Options);
compile(FileOrTemplate, Module, Options) when is_binary(FileOrTemplate) ->
compile_template(FileOrTemplate, Module, Options);
compile(FileOrTemplate, Module, Options) ->
compile_file(FileOrTemplate, Module, Options).
erlydtl-0.15.0/src/erlydtl_beam_compiler.erl 0000664 0000000 0000000 00000233473 15047124314 0021121 0 ustar 00root root 0000000 0000000 %%%-------------------------------------------------------------------
%%% File: erlydtl_beam_compiler.erl
%%% @author Roberto Saccon [http://rsaccon.com]
%%% @author Evan Miller
%%% @author Andreas Stenius
%%% @copyright 2008 Roberto Saccon, Evan Miller
%%% @copyright 2014 Andreas Stenius
%%% @doc
%%% ErlyDTL template compiler for beam targets.
%%% @end
%%%
%%% The MIT License
%%%
%%% Copyright (c) 2007 Roberto Saccon, Evan Miller
%%% Copyright (c) 2014 Andreas Stenius
%%%
%%% 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.
%%%
%%% @since 2007-12-16 by Roberto Saccon, Evan Miller
%%% @since 2014 by Andreas Stenius
%%%-------------------------------------------------------------------
-module(erlydtl_beam_compiler).
-author('rsaccon@gmail.com').
-author('emmiller@gmail.com').
-author('Andreas Stenius ').
%% --------------------------------------------------------------------
%% Definitions
%% --------------------------------------------------------------------
-export([compile/3, compile_dir/2, format_error/1]).
%% internal use
-export([
is_up_to_date/2,
format/1,
value_ast/4,
interpret_args/2
]).
-import(erlydtl_compiler, [parse_file/2, do_parse_template/2]).
-import(erlydtl_compiler_utils,
[unescape_string_literal/1, full_path/2, push_scope/2,
restore_scope/2, begin_scope/1, begin_scope/2, end_scope/4,
empty_scope/0, get_current_file/1, add_errors/2,
add_warnings/2, merge_info/2, call_extension/3,
init_treewalker/1, resolve_variable/2, resolve_variable/3,
reset_block_dict/2, reset_parse_trail/2, load_library/3,
load_library/4, shorten_filename/2, push_auto_escape/2,
pop_auto_escape/1, token_pos/1, is_stripped_token_empty/1]).
-ifdef(MERL_DEP).
-include_lib("merl/include/merl.hrl").
-else.
-include_lib("syntax_tools/include/merl.hrl").
-endif.
-include("erlydtl_ext.hrl").
%% --------------------------------------------------------------------
%% API
%% --------------------------------------------------------------------
compile(DjangoParseTree, CheckSum, Context) ->
compile_to_binary(DjangoParseTree, CheckSum, Context).
compile_dir(Dir, Context) ->
do_compile_dir(Dir, Context).
format_error(no_out_dir) ->
"Compiled template not saved (need out_dir option)";
format_error(unexpected_extends_tag) ->
"The extends tag must be at the very top of the template";
format_error(circular_include) ->
"Circular file inclusion!";
format_error({write_file, Error}) ->
io_lib:format(
"Failed to write file: ~s",
[file:format_error(Error)]);
format_error(compile_beam) ->
"Failed to compile template to BEAM code";
format_error({unknown_filter, Name, Arity}) ->
io_lib:format("Unknown filter '~s' (arity ~b)", [Name, Arity]);
format_error({filter_args, Name, {Mod, Fun}, Arity}) ->
io_lib:format("Wrong number of arguments to filter '~s' (~s:~s): ~b", [Name, Mod, Fun, Arity]);
format_error({unknown_tag, Name}) ->
io_lib:format("Unknown tag '~s'", [Name]);
format_error({missing_tag, Name, {Mod, Fun}}) ->
io_lib:format("Custom tag '~s' not exported (~s:~s)", [Name, Mod, Fun]);
format_error({bad_tag, Name, {Mod, Fun}, Arity}) ->
io_lib:format("Invalid tag '~s' (~s:~s/~b)", [Name, Mod, Fun, Arity]);
format_error({load_code, Error}) ->
io_lib:format("Failed to load BEAM code: ~p", [Error]);
format_error({reserved_variable, ReservedName}) ->
io_lib:format("Variable '~s' is reserved for internal use.", [ReservedName]);
format_error({translation_fun, Fun}) ->
io_lib:format("Invalid translation function: ~s~n",
[if is_function(Fun) ->
Info = erlang:fun_info(Fun),
io_lib:format("~s:~s/~b", [proplists:get_value(K, Info) || K <- [module, name, arity]]);
true -> io_lib:format("~p", [Fun])
end]);
format_error(non_block_tag) ->
"Non-block tag in extends-template.";
format_error(Error) ->
erlydtl_compiler:format_error(Error).
%%====================================================================
%% Internal functions
%%====================================================================
do_compile_dir(Dir, Context) ->
%% Find all files in Dir (recursively), matching the regex (no
%% files ending in "~").
Files = filelib:fold_files(Dir, ".+[^~]$", true, fun(F1,Acc1) -> [F1 | Acc1] end, []),
{ParserResults,
#dtl_context{ errors=#error_info{ list=ParserErrors } }=Context1}
= lists:foldl(
fun (File, {ResultAcc, Ctx}) ->
case filename:basename(File) of
"."++_ ->
{ResultAcc, Ctx};
_ ->
FilePath = filename:absname(File),
case filelib:is_dir(FilePath) of
true ->
{ResultAcc, Ctx};
false ->
case parse_file(FilePath, Ctx) of
up_to_date -> {ResultAcc, Ctx};
{ok, DjangoParseTree, CheckSum} ->
{[{FilePath, DjangoParseTree, CheckSum}|ResultAcc], Ctx};
{error, Reason} -> {ResultAcc, ?ERR(Reason, Ctx)}
end
end
end
end,
{[], Context},
Files),
if length(ParserErrors) == 0 ->
compile_multiple_to_binary(Dir, ParserResults, Context1);
true -> Context1
end.
compile_multiple_to_binary(Dir, ParserResults, Context) ->
MatchAst = options_match_ast(Context),
{Functions,
{AstInfo,
#treewalker{
context=#dtl_context{
errors=#error_info{ list=Errors }
}=Context1 } }
} = lists:mapfoldl(
fun ({File, DjangoParseTree, CheckSum},
{AstInfo, #treewalker{ context=Ctx }=TreeWalker}) ->
try
FilePath = full_path(File, Ctx#dtl_context.doc_root),
{{BodyAst, BodyInfo}, TreeWalker1} = with_dependency(
{FilePath, CheckSum},
body_ast(DjangoParseTree, TreeWalker)),
FunctionName = filename:rootname(filename:basename(File)),
FunctionDefs = ?Q(["'@func'(Variables) -> _@func(Variables, []).",
"'@func'(_Variables, RenderOptions) ->",
" _@MatchAst, _@body."],
[{func, erl_syntax:atom(FunctionName)},
{body, stringify(BodyAst, Ctx)}]),
{{FunctionName, FunctionDefs}, {merge_info(AstInfo, BodyInfo), TreeWalker1}}
catch
throw:Error ->
{error, {AstInfo, TreeWalker#treewalker{ context=?ERR(Error, Ctx) }}}
end
end,
{#ast_info{}, init_treewalker(Context)},
ParserResults),
if length(Errors) == 0 ->
Forms = custom_forms(Dir, Context1#dtl_context.module, Functions, AstInfo),
compile_forms(Forms, Context1);
true ->
Context1
end.
compile_to_binary(DjangoParseTree, CheckSum, Context) ->
try body_ast(DjangoParseTree, init_treewalker(Context)) of
{{BodyAst, BodyInfo}, BodyTreeWalker} ->
try custom_tags_ast(BodyInfo#ast_info.custom_tags, BodyTreeWalker) of
{CustomTags,
#treewalker{
context=#dtl_context{
errors=#error_info{ list=Errors }
} }=CustomTagsTreeWalker}
when length(Errors) == 0 ->
Forms = forms(
{BodyAst, BodyInfo},
CustomTags, CheckSum,
CustomTagsTreeWalker),
compile_forms(Forms, CustomTagsTreeWalker#treewalker.context);
{_, #treewalker{ context=Context1 }} ->
Context1
catch
throw:Error -> ?ERR(Error, BodyTreeWalker#treewalker.context)
end
catch
throw:Error -> ?ERR(Error, Context)
end.
compile_forms(Forms, Context) ->
maybe_debug_template(Forms, Context),
Options = Context#dtl_context.compiler_options,
case compile:forms(Forms, [nowarn_shadow_vars|Options]) of
Compiled when element(1, Compiled) =:= ok ->
[ok, Module, Bin|Info] = tuple_to_list(Compiled),
lists:foldl(
fun (F, C) -> F(Module, Bin, C) end,
Context#dtl_context{ bin=Bin },
[fun maybe_write/3,
fun maybe_load/3,
fun (_, _, C) ->
case Info of
[Ws] when length(Ws) > 0 ->
add_warnings(Ws, C);
_ -> C
end
end
]);
error ->
?ERR(compile_beam, Context);
{error, Es, Ws} ->
add_warnings(Ws, add_errors(Es, Context))
end.
maybe_write(Module, Bin, Context) ->
case proplists:get_value(out_dir, Context#dtl_context.all_options) of
false -> Context;
undefined ->
?WARN(no_out_dir, Context);
OutDir ->
BeamFile = filename:join([OutDir, [Module, ".beam"]]),
?LOG_INFO("Template module: ~w -> ~s\n", [Module, BeamFile], Context),
case file:write_file(BeamFile, Bin) of
ok -> Context;
{error, Reason} ->
?ERR({write_file, Reason}, Context)
end
end.
maybe_load(Module, Bin, Context) ->
case proplists:get_bool(no_load, Context#dtl_context.all_options) of
true -> Context;
false -> load_code(Module, Bin, Context)
end.
load_code(Module, Bin, Context) ->
code:purge(Module),
case code:load_binary(Module, atom_to_list(Module) ++ ".erl", Bin) of
{module, Module} -> Context;
Error -> ?WARN({load_code, Error}, Context)
end.
maybe_debug_template(Forms, Context) ->
case proplists:get_bool(debug_compiler, Context#dtl_context.all_options) of
false -> nop;
true ->
Options = Context#dtl_context.compiler_options,
?LOG_DEBUG("Compiler options: ~p~n", [Options], Context),
try
Source = erl_prettypr:format(
erl_syntax:form_list(Forms),
[{ribbon, 100}, {paper, 200}]),
SourceFile = lists:concat(
[proplists:get_value(source, Options),".erl"]),
File = case proplists:get_value(
debug_root,
Context#dtl_context.all_options) of
false -> undefined;
undefined -> SourceFile;
Dir ->
Abs = filename:absname(
shorten_filename(
SourceFile,
Context#dtl_context.doc_root),
Dir),
case filelib:ensure_dir(Abs) of
ok -> Abs;
{error, Reason} ->
io:format(
"Failed to ensure directories for file '~s': ~p~n",
[Abs, Reason]),
undefined
end
end,
if File =/= undefined ->
io:format("Saving template source to: ~s.. ~p~n",
[File, file:write_file(File, Source)]);
true -> ok
end
catch
error:Err ->
io:format("Pretty printing failed: ~p~n"
"Context: ~n~p~n"
"Forms: ~n~p~n",
[Err, Context, Forms])
end
end.
is_up_to_date(CheckSum, Context) ->
Module = Context#dtl_context.module,
{M, F} = Context#dtl_context.reader,
ReaderOptions = Context#dtl_context.reader_options,
case catch Module:source() of
{_, CheckSum} ->
case catch Module:dependencies() of
L when is_list(L) ->
RecompileList = lists:foldl(
fun ({XFile, XCheckSum}, Acc) ->
case catch erlydtl_runtime:read_file_internal(M, F, XFile, ReaderOptions) of
{ok, Data} ->
case binary_to_list(erlang:md5(Data)) of
XCheckSum ->
Acc;
_ ->
[recompile | Acc]
end;
_ ->
[recompile | Acc]
end
end, [], L),
case RecompileList of
[] -> true;
_ -> false
end;
_ ->
false
end;
_ ->
false
end.
%%====================================================================
%% AST functions
%%====================================================================
custom_tags_ast(CustomTags, TreeWalker) ->
%% avoid adding the render_tag/3 fun if it isn't used,
%% since we can't add a -compile({nowarn_unused_function, render_tag/3}).
%% attribute due to a bug in syntax_tools.
case custom_tags_clauses_ast(CustomTags, TreeWalker) of
skip ->
{{erl_syntax:comment(
["%% render_tag/3 is not used in this template."]),
#ast_info{}},
TreeWalker};
{{CustomTagsClauses, CustomTagsInfo}, TreeWalker1} ->
{{erl_syntax:function(
erl_syntax:atom(render_tag),
CustomTagsClauses),
CustomTagsInfo},
TreeWalker1}
end.
custom_tags_clauses_ast([], _TreeWalker) -> skip;
custom_tags_clauses_ast(CustomTags, TreeWalker) ->
custom_tags_clauses_ast1(CustomTags, [], [], #ast_info{}, TreeWalker).
custom_tags_clauses_ast1([], _ExcludeTags, ClauseAcc, InfoAcc, TreeWalker) ->
{{DefaultAst, DefaultInfo}, TreeWalker1} =
case call_extension(TreeWalker, custom_tag_ast, [TreeWalker]) of
undefined ->
{{?Q("(_TagName, _, _) -> []"), InfoAcc}, TreeWalker};
{{ExtAst, ExtInfo}, ExtTreeWalker} ->
Clause = ?Q("(TagName, _Variables, RenderOptions) -> _@tag",
[{tag, options_match_ast(ExtTreeWalker) ++ [ExtAst]}]),
{{Clause, merge_info(ExtInfo, InfoAcc)}, ExtTreeWalker}
end,
{{lists:reverse([DefaultAst|ClauseAcc]), DefaultInfo}, TreeWalker1};
custom_tags_clauses_ast1([Tag|CustomTags], ExcludeTags, ClauseAcc, InfoAcc, TreeWalker) ->
case lists:member(Tag, ExcludeTags) of
true ->
custom_tags_clauses_ast1(CustomTags, ExcludeTags, ClauseAcc, InfoAcc, TreeWalker);
false ->
Context = TreeWalker#treewalker.context,
CustomTagFile = full_path(Tag, Context#dtl_context.custom_tags_dir),
case filelib:is_file(CustomTagFile) of
true ->
case parse_file(CustomTagFile, Context) of
{ok, DjangoParseTree, CheckSum} ->
{{BodyAst, BodyAstInfo}, TreeWalker1} = with_dependency(
{CustomTagFile, CheckSum},
body_ast(DjangoParseTree, TreeWalker)),
MatchAst = options_match_ast(TreeWalker1),
Clause = ?Q("(_@Tag@, _Variables, RenderOptions) -> _@MatchAst, _@BodyAst"),
custom_tags_clauses_ast1(
CustomTags, [Tag|ExcludeTags], [Clause|ClauseAcc],
merge_info(BodyAstInfo, InfoAcc), TreeWalker1);
{error, Reason} ->
empty_ast(?ERR(Reason, TreeWalker))
end;
false ->
case call_extension(TreeWalker, custom_tag_ast, [Tag, TreeWalker]) of
undefined ->
custom_tags_clauses_ast1(
CustomTags, [Tag | ExcludeTags],
ClauseAcc, InfoAcc,
?WARN({unknown_tag, Tag}, TreeWalker)
);
{{Ast, Info}, TW} ->
Clause = ?Q("(_@Tag@, _Variables, RenderOptions) -> _@match, _@Ast",
[{match, options_match_ast(TW)}]),
custom_tags_clauses_ast1(
CustomTags, [Tag | ExcludeTags], [Clause|ClauseAcc],
merge_info(Info, InfoAcc), TW)
end
end
end.
custom_forms(Dir, Module, Functions, AstInfo) ->
Dependencies = AstInfo#ast_info.dependencies,
TranslatableStrings = AstInfo#ast_info.translatable_strings,
TranslatedBlocks = AstInfo#ast_info.translated_blocks,
Variables = lists:usort(AstInfo#ast_info.var_names),
DefaultVariables = lists:usort(AstInfo#ast_info.def_names),
Constants = lists:usort(AstInfo#ast_info.const_names),
erl_syntax:revert_forms(
lists:flatten(
?Q(["-module('@Module@').",
"-ignore_xref('@Module@').",
"-export([source_dir/0, dependencies/0, translatable_strings/0,",
" translated_blocks/0, variables/0, default_variables/0,",
" constants/0, render/1, render/2, render/3]).",
"-export(['@__export_functions'/0]).",
"source_dir() -> _@Dir@.",
"dependencies() -> _@Dependencies@.",
"variables() -> _@Variables@.",
"default_variables() -> _@DefaultVariables@.",
"constants() -> _@Constants@.",
"translatable_strings() -> _@TranslatableStrings@.",
"translated_blocks() -> _@TranslatedBlocks@.",
"render(Tag) -> render(Tag, [], []).",
"render(Tag, Vars) -> render(Tag, Vars, []).",
"render(Tag, Vars, Opts) ->",
" try '@Module@':Tag(Vars, Opts) of",
" Val -> {ok, Val}",
" catch",
" Err -> {error, Err}",
" end.",
"'@_functions'() -> _."
],
[{export_functions,
erl_syntax:list(
[erl_syntax:arity_qualifier(erl_syntax:atom(FName), erl_syntax:integer(Arity))
|| {FName, _} <- Functions, Arity <- [1, 2]])},
{functions, [Ast || {_, Ast} <- Functions]}
]))
).
stringify(BodyAst, #dtl_context{ binary_strings=BinaryStrings }) ->
[?Q("erlydtl_runtime:stringify_final(_@BodyAst, '@BinaryStrings@')")].
forms({BodyAst, BodyInfo}, {CustomTagsFunctionAst, CustomTagsInfo}, CheckSum,
#treewalker{
context=#dtl_context{
module=Module,
parse_trail=[File|_]
}=Context
}=TreeWalker) ->
MergedInfo = merge_info(BodyInfo, CustomTagsInfo),
Dependencies = MergedInfo#ast_info.dependencies,
TranslatableStrings = MergedInfo#ast_info.translatable_strings,
TranslatedBlocks = MergedInfo#ast_info.translated_blocks,
Variables = lists:usort(MergedInfo#ast_info.var_names),
DefaultVariables = lists:usort(MergedInfo#ast_info.def_names),
Constants = lists:usort(MergedInfo#ast_info.const_names),
FinalBodyAst = options_match_ast(TreeWalker) ++ stringify(BodyAst, Context),
erl_syntax:revert_forms(
?Q(["-module('@Module@').",
"-ignore_xref('@Module@').",
"-export([render/0, render/1, render/2, source/0, dependencies/0,",
" translatable_strings/0, translated_blocks/0, variables/0,",
" default_variables/0, constants/0]).",
"source() -> {_@File@, _@CheckSum@}.",
"dependencies() -> _@Dependencies@.",
"variables() -> _@Variables@.",
"default_variables() -> _@DefaultVariables@.",
"constants() -> _@Constants@.",
"translatable_strings() -> _@TranslatableStrings@.",
"translated_blocks() -> _@TranslatedBlocks@.",
"'@_CustomTagsFunctionAst'() -> _.",
"render() -> render([], []).",
"render(Variables) -> render(Variables, []).",
"render(Variables, RenderOptions) ->",
" try render_internal(Variables, RenderOptions) of",
" Val -> {ok, Val}",
" catch",
" Err -> {error, Err}",
" end.",
"render_internal(_Variables, RenderOptions) -> _@FinalBodyAst."
])).
options_match_ast(#treewalker{ context=Context }=TreeWalker) ->
options_match_ast(Context, TreeWalker);
options_match_ast(Context) ->
options_match_ast(Context, undefined).
options_match_ast(Context, TreeWalker) ->
[
?Q(["_TranslationFun = erlydtl_runtime:init_translation(",
" proplists:get_value(translation_fun, RenderOptions, none))"]),
?Q("_CurrentLocale = proplists:get_value(locale, RenderOptions, default)"),
?Q("_RecordInfo = _@info", [{info, merl:term(Context#dtl_context.record_info)}])
| case call_extension(Context, setup_render_ast, [Context, TreeWalker]) of
undefined -> [];
Ast when is_list(Ast) -> Ast
end].
%% child templates should only consist of blocks at the top level
body_ast([{'extends', {string_literal, _Pos, String}} | ThisParseTree], #treewalker{ context=Context }=TreeWalker) ->
ThisFile = get_current_file(Context),
File = full_path(unescape_string_literal(String), Context#dtl_context.doc_root),
case lists:member(File, Context#dtl_context.parse_trail) of
true ->
empty_ast(?ERR(circular_include, TreeWalker));
_ ->
case parse_file(File, Context) of
{ok, ParentParseTree, CheckSum} ->
{BlockDict, Context1} = lists:foldl(
fun ({block, {identifier, Pos, Name}, Contents}, {Dict, Ctx}) ->
{dict:store(Name, [{ThisFile, Pos, Contents}], Dict), Ctx};
(Token, {Dict, Ctx}) ->
case proplists:get_bool(non_block_tag, Ctx#dtl_context.checks) of
true ->
case is_stripped_token_empty(Token) of
false ->
{Dict, ?WARN({token_pos(Token), non_block_tag}, Ctx)};
true ->
{Dict, Ctx}
end;
false ->
{Dict, Ctx}
end
end,
{dict:new(), Context},
ThisParseTree),
{Info, TreeWalker1} = with_dependency(
{File, CheckSum},
body_ast(
ParentParseTree,
TreeWalker#treewalker{
context=Context1#dtl_context{
block_dict = dict:merge(
fun(_Key, ParentVal, ChildVal) ->
ChildVal ++ ParentVal
end,
BlockDict, Context#dtl_context.block_dict),
parse_trail = [File | Context1#dtl_context.parse_trail]
}
})),
{Info, reset_parse_trail(
Context1#dtl_context.parse_trail,
reset_block_dict(
Context1#dtl_context.block_dict,
TreeWalker1))};
{error, Reason} ->
empty_ast(?ERR(Reason, TreeWalker))
end
end;
body_ast(DjangoParseTree, TreeWalker) ->
ScopeFun = fun ([ScopeVars|ScopeBody]) -> [?Q("(fun() -> _@ScopeVars, [_@ScopeBody] end)()")] end,
body_ast(DjangoParseTree, empty_scope(), ScopeFun, TreeWalker).
body_ast(DjangoParseTree, BodyScope, ScopeFun, TreeWalker) ->
{ScopeId, TreeWalkerScope} = begin_scope(BodyScope, TreeWalker),
BodyFun =
fun ({'autoescape', {identifier, _, OnOrOff}, Contents}, TW) ->
{Info, BodyTW} = body_ast(Contents, push_auto_escape(OnOrOff, TW)),
{Info, pop_auto_escape(BodyTW)};
({'block', {identifier, _Pos, Name}, Contents}, #treewalker{ context=Context }=TW) ->
ContentsAst = body_ast(Contents, TW),
case dict:find(Name, Context#dtl_context.block_dict) of
{ok, ChildBlocks} ->
lists:foldr(
fun ({ChildFile, ChildPos, ChildBlock}, {{SuperAst, SuperInfo}, AccTW}) ->
BlockScope = create_scope(
[{block, ?Q("fun (super) -> _@SuperAst; (_) -> [] end"), safe}],
ChildPos, ChildFile, AccTW),
{{BlockAst, BlockInfo}, BlockTW} = body_ast(ChildBlock, BlockScope, ScopeFun, AccTW),
{{BlockAst, merge_info(SuperInfo, BlockInfo)}, BlockTW}
end,
ContentsAst, ChildBlocks);
_ ->
ContentsAst
end;
({'blocktrans', Args, Contents, PluralContents}, TW) ->
blocktrans_ast(Args, Contents, PluralContents, TW);
({'call', {identifier, _, Name}}, TW) ->
call_ast(Name, TW);
({'call', {identifier, _, Name}, With}, TW) ->
call_with_ast(Name, With, TW);
({'comment', _Contents}, TW) ->
empty_ast(TW);
({'comment_tag', _, _}, TW) ->
empty_ast(TW);
({'cycle', Names, AsVar}, TW) ->
cycle_ast(Names, AsVar, TW);
({'cycle_compat', Names}, TW) ->
cycle_compat_ast(Names, TW);
({'date', 'now', {string_literal, _Pos, FormatString}}, TW) ->
now_ast(FormatString, TW);
({'filter', FilterList, Contents}, TW) ->
filter_tag_ast(FilterList, Contents, TW);
({'firstof', Vars}, TW) ->
firstof_ast(Vars, TW);
({'for', {'in', IteratorList, Variable, Reversed}, Contents}, TW) ->
{EmptyAstInfo, TW1} = empty_ast(TW),
for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, TW1);
({'for', {'in', IteratorList, Variable, Reversed}, Contents, EmptyPartContents}, TW) ->
{EmptyAstInfo, TW1} = body_ast(EmptyPartContents, TW),
for_loop_ast(IteratorList, Variable, Reversed, Contents, EmptyAstInfo, TW1);
({'if', Expression, Contents, Elif}, TW) ->
{IfAstInfo, TW1} = body_ast(Contents, TW),
{ElifAstInfo, TW2} = body_ast(Elif, TW1),
ifelse_ast(Expression, IfAstInfo, ElifAstInfo, TW2);
({'if', Expression, Contents}, TW) ->
{IfAstInfo, TW1} = body_ast(Contents, TW),
{ElseAstInfo, TW2} = empty_ast(TW1),
ifelse_ast(Expression, IfAstInfo, ElseAstInfo, TW2);
({'ifchanged', '$undefined', Contents}, TW) ->
{IfAstInfo, TW1} = body_ast(Contents, TW),
{ElseAstInfo, TW2} = empty_ast(TW1),
ifchanged_contents_ast(Contents, IfAstInfo, ElseAstInfo, TW2);
({'ifchanged', Values, Contents}, TW) ->
{IfAstInfo, TW1} = body_ast(Contents, TW),
{ElseAstInfo, TW2} = empty_ast(TW1),
ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, TW2);
({'ifchangedelse', '$undefined', IfContents, ElseContents}, TW) ->
{IfAstInfo, TW1} = body_ast(IfContents, TW),
{ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
ifchanged_contents_ast(IfContents, IfAstInfo, ElseAstInfo, TW2);
({'ifchangedelse', Values, IfContents, ElseContents}, TW) ->
{IfAstInfo, TW1} = body_ast(IfContents, TW),
{ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
ifchanged_values_ast(Values, IfAstInfo, ElseAstInfo, TW2);
({'ifelse', Expression, IfContents, ElseContents}, TW) ->
{IfAstInfo, TW1} = body_ast(IfContents, TW),
{ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
ifelse_ast(Expression, IfAstInfo, ElseAstInfo, TW2);
({'ifequal', [Arg1, Arg2], Contents}, TW) ->
{IfAstInfo, TW1} = body_ast(Contents, TW),
{ElseAstInfo, TW2} = empty_ast(TW1),
ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
({'ifequalelse', [Arg1, Arg2], IfContents, ElseContents}, TW) ->
{IfAstInfo, TW1} = body_ast(IfContents, TW),
{ElseAstInfo, TW2} = body_ast(ElseContents,TW1),
ifelse_ast({'expr', "eq", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
({'ifnotequal', [Arg1, Arg2], Contents}, TW) ->
{IfAstInfo, TW1} = body_ast(Contents, TW),
{ElseAstInfo, TW2} = empty_ast(TW1),
ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
({'ifnotequalelse', [Arg1, Arg2], IfContents, ElseContents}, TW) ->
{IfAstInfo, TW1} = body_ast(IfContents, TW),
{ElseAstInfo, TW2} = body_ast(ElseContents, TW1),
ifelse_ast({'expr', "ne", Arg1, Arg2}, IfAstInfo, ElseAstInfo, TW2);
({'include', {string_literal, _, File}, Args}, #treewalker{ context=Context }=TW) ->
include_ast(unescape_string_literal(File), Args, Context#dtl_context.local_scopes, TW);
({'include_only', {string_literal, _, File}, Args}, TW) ->
{Info, IncTW} = include_ast(unescape_string_literal(File), Args, [], TW),
{Info, restore_scope(TW, IncTW)};
({'load_libs', Libs}, TW) ->
load_libs_ast(Libs, TW);
({'load_from_lib', What, Lib}, TW) ->
load_from_lib_ast(What, Lib, TW);
({'regroup', {ListVariable, Grouper, {identifier, _, NewVariable}}}, TW) ->
regroup_ast(ListVariable, Grouper, NewVariable, TW);
('end_regroup', TW) ->
{{end_scope, #ast_info{}}, TW};
({'spaceless', Contents}, TW) ->
spaceless_ast(Contents, TW);
({'ssi', Arg}, TW) ->
ssi_ast(Arg, TW);
({'ssi_parsed', {string_literal, _, FileName}}, #treewalker{ context=Context }=TW) ->
include_ast(unescape_string_literal(FileName), [], Context#dtl_context.local_scopes, TW);
({'string', _Pos, String}, TW) ->
string_ast(String, TW);
({'tag', Name, Args}, TW) ->
tag_ast(Name, Args, TW);
({'tag', Name, Args, {identifier, _, NewTagVar}}, TW) ->
tag_ast(Name, Args, NewTagVar, TW);
({'templatetag', {_, _, TagName}}, TW) ->
templatetag_ast(TagName, TW);
({'trans', Value}, TW) ->
translated_ast(Value, TW);
({'trans', Value, Context}, TW) ->
translated_ast(Value, Context, TW);
({'widthratio', Numerator, Denominator, Scale}, TW) ->
widthratio_ast(Numerator, Denominator, Scale, TW);
({'with', Args, Contents}, TW) ->
with_ast(Args, Contents, TW);
({'scope_as', {identifier, _, Name}, Contents}, TW) ->
scope_as(Name, Contents, TW);
({'extension', Tag}, TW) ->
extension_ast(Tag, TW);
({'extends', _}, TW) ->
empty_ast(?ERR(unexpected_extends_tag, TW));
({'language', Locale, Contents}, TW) ->
{{LocaleAst, LocaleInfo}, LocaleTW} = value_ast(Locale, true, false, TW),
LanguageScopeFun = fun ([ScopeVars|ScopeBody]) -> [?Q("(fun(_CurrentLocale) -> _@ScopeVars, [_@ScopeBody] end)(_@LocaleAst)")] end,
{{BodyAst, BodyInfo}, BodyTW} = body_ast(Contents, {[], [?Q("")]}, LanguageScopeFun, LocaleTW),
{{BodyAst, merge_info(BodyInfo, LocaleInfo)}, BodyTW};
(ValueToken, TW) ->
format(value_ast(ValueToken, true, true, TW))
end,
{AstInfoList, TreeWalker1} = lists:mapfoldl(BodyFun, TreeWalkerScope, DjangoParseTree),
{AstList, Info} =
lists:mapfoldl(
fun ({Ast, Info}, InfoAcc) ->
{Ast, merge_info(Info, InfoAcc)}
end, #ast_info{}, AstInfoList),
{Ast, TreeWalker2} = end_scope(ScopeFun, ScopeId, AstList, TreeWalker1),
{{erl_syntax:list(Ast), Info}, TreeWalker2}.
value_ast(ValueToken, AsString, EmptyIfUndefined, TreeWalker) ->
case ValueToken of
{'expr', Operator, Value} ->
{{ValueAst,InfoValue}, TreeWalker1} = value_ast(Value, false, EmptyIfUndefined, TreeWalker),
Op = list_to_atom(Operator),
Ast = ?Q("erlydtl_runtime:_@Op@(_@ValueAst)"),
{{Ast, InfoValue}, TreeWalker1};
{'expr', Operator, Value1, Value2} ->
{{Value1Ast,InfoValue1}, TreeWalker1} = value_ast(Value1, false, EmptyIfUndefined, TreeWalker),
{{Value2Ast,InfoValue2}, TreeWalker2} = value_ast(Value2, false, EmptyIfUndefined, TreeWalker1),
Op = list_to_atom(Operator),
Ast = ?Q("erlydtl_runtime:_@Op@(_@Value1Ast, _@Value2Ast)"),
{{Ast, merge_info(InfoValue1,InfoValue2)}, TreeWalker2};
{'string_literal', _Pos, String} ->
string_ast(unescape_string_literal(String), TreeWalker);
{'number_literal', _Pos, Number} ->
case AsString of
true -> string_ast(Number, TreeWalker);
false -> {{erl_syntax:integer(Number), #ast_info{}}, TreeWalker}
end;
{'apply_filter', Variable, Filter} ->
filter_ast(Variable, Filter, TreeWalker);
{'attribute', _} = Variable ->
resolve_variable_ast(Variable, EmptyIfUndefined, TreeWalker);
{'variable', _} = Variable ->
resolve_variable_ast(Variable, EmptyIfUndefined, TreeWalker);
{'trans', Value} ->
translated_ast(Value, TreeWalker);
{extension, Tag} ->
extension_ast(Tag, TreeWalker)
end.
extension_ast(Tag, TreeWalker) ->
case call_extension(TreeWalker, compile_ast, [Tag, TreeWalker]) of
undefined ->
empty_ast(?WARN({unknown_extension, Tag}, TreeWalker));
Result ->
Result
end.
with_dependencies([], Ast) -> Ast;
with_dependencies([Dependency | Rest], Ast) ->
with_dependencies(Rest, with_dependency(Dependency, Ast)).
with_dependency(FilePath, {{Ast, Info}, TreeWalker}) ->
Dependencies = [FilePath | Info#ast_info.dependencies],
{{Ast, Info#ast_info{ dependencies = Dependencies }}, TreeWalker}.
empty_ast(TreeWalker) ->
{{erl_syntax:list([]), #ast_info{}}, TreeWalker}.
%%% Note: Context here refers to the translation context, not the #dtl_context{} record
blocktrans_ast(Args, Contents, PluralContents, TreeWalker) ->
%% get args, count and context
ArgList = [{Name, Value}
|| {{identifier, _, Name}, Value}
<- proplists:get_value(args, Args, [])],
Count = proplists:get_value(count, Args),
Context = case proplists:get_value(context, Args) of
undefined -> undefined;
{string_literal, _, S} ->
unescape_string_literal(S)
end,
Trimmed = proplists:get_value(trimmed, Args),
%% add new scope using 'with' values
{NewScope, {ArgInfo, TreeWalker1}} =
lists:mapfoldl(
fun ({LocalVarName, Value}, {AstInfoAcc, TreeWalkerAcc}) ->
{{Ast, Info}, TW} = value_ast(Value, false, false, TreeWalkerAcc),
{{LocalVarName, Ast}, {merge_info(AstInfoAcc, Info), TW}}
end,
{#ast_info{}, TreeWalker},
case Count of
{{identifier, _, Name}, Value} ->
[{Name, Value}|ArgList];
_ ->
ArgList
end),
TreeWalker2 = push_scope(NewScope, TreeWalker1),
%% key for translation lookup
SourceText = erlydtl_unparser:unparse(Contents, Trimmed),
{{DefaultAst, AstInfo}, TreeWalker3} = body_ast(Contents, TreeWalker2),
MergedInfo = merge_info(AstInfo, ArgInfo),
#dtl_context{
trans_fun = TFun,
trans_locales = TLocales, auto_escape = AutoEscape } = TreeWalker3#treewalker.context,
if TFun =:= none; PluralContents =/= undefined ->
%% translate in runtime
{FinalAst, FinalTW} = blocktrans_runtime_ast(
{DefaultAst, MergedInfo}, SourceText, Contents, Context, AutoEscape,
plural_contents(PluralContents, Count, TreeWalker3), Trimmed),
{FinalAst, restore_scope(TreeWalker1, FinalTW)};
is_function(TFun, 2) ->
%% translate in compile-time
{FinalAstInfo, FinalTreeWalker, Clauses} =
lists:foldr(
fun (Locale, {AstInfoAcc, TreeWalkerAcc, ClauseAcc}) ->
case TFun(SourceText, phrase_locale(Locale, Context)) of
default ->
{AstInfoAcc, TreeWalkerAcc, ClauseAcc};
Body ->
{ok, DjangoParseTree} = do_parse_template(Body, TreeWalkerAcc#treewalker.context),
{{BodyAst, BodyInfo}, BodyTreeWalker} = body_ast(DjangoParseTree, TreeWalkerAcc),
Clause = erl_syntax:clause([erl_syntax:abstract(Locale)], none, [BodyAst]),
{merge_info(BodyInfo, AstInfoAcc), BodyTreeWalker,
[Clause | ClauseAcc]}
end
end,
{MergedInfo, TreeWalker3, []}, TLocales),
FinalAst = ?Q("case _CurrentLocale of _@_Clauses -> _; _ -> _@DefaultAst end"),
{{FinalAst, FinalAstInfo#ast_info{ translated_blocks = [SourceText] }},
restore_scope(TreeWalker1, FinalTreeWalker)};
true ->
empty_ast(?ERR({translation_fun, TFun}, TreeWalker3))
end.
blocktrans_runtime_ast({DefaultAst, Info}, SourceText, Contents, Context, AutoEscape, {Plural, TreeWalker}, Trimmed) ->
%% Contents is flat - only strings and '{{var}}' allowed.
%% build sorted list (orddict) of pre-resolved variables to pass to runtime translation function
USortedVariables = lists:usort(fun({variable, {identifier, _, A}},
{variable, {identifier, _, B}}) ->
A =< B
end, [Var || {variable, _}=Var
<- Contents ++ maybe_plural_contents(Plural)]),
VarBuilder = fun({variable, {identifier, _, Name}}=Var, TW) ->
{{VarAst, _VarInfo}, VarTW} = resolve_variable_ast(Var, false, TW),
{?Q("{_@name, _@VarAst}", [{name, merl:term(atom_to_list(Name))}]), VarTW}
end,
{VarAsts, TreeWalker1} = lists:mapfoldl(VarBuilder, TreeWalker, USortedVariables),
VarListAst = erl_syntax:list(VarAsts),
BlockTransAst = ?Q(["begin",
" case erlydtl_runtime:translate_block(",
" _@phrase, _@locale, _@auto_escape, ",
" _@VarListAst, _TranslationFun) of",
" default -> _@DefaultAst;",
" Text -> Text",
" end",
"end"],
[{phrase, phrase_ast(SourceText, Plural, Trimmed)},
{auto_escape, autoescape_ast(AutoEscape)},
{locale, phrase_locale_ast(Context)}]),
{{BlockTransAst, merge_count_info(Info, Plural)}, TreeWalker1}.
maybe_plural_contents(undefined) -> [];
maybe_plural_contents({Contents, _}) -> Contents.
merge_count_info(Info, undefined) -> Info;
merge_count_info(Info, {_Contents, {_CountAst, CountInfo}}) ->
merge_info(Info, CountInfo).
plural_contents(undefined, _, TreeWalker) -> {undefined, TreeWalker};
plural_contents(Contents, {_CountVarName, Value}, TreeWalker) ->
{CountAst, TW} = value_ast(Value, false, false, TreeWalker),
{{Contents, CountAst}, TW}.
phrase_ast(Text, undefined, _) -> merl:term(Text);
phrase_ast(Text, {Contents, {CountAst, _CountInfo}}, Trimmed) ->
erl_syntax:tuple(
[merl:term(Text),
erl_syntax:tuple(
[merl:term(erlydtl_unparser:unparse(Contents, Trimmed)),
CountAst])
]).
autoescape_ast([]) -> autoescape_ast([on]);
autoescape_ast([V | _]) -> erl_syntax:atom(V == on).
phrase_locale_ast(undefined) -> merl:var('_CurrentLocale');
phrase_locale_ast(Context) -> erl_syntax:tuple([merl:var('_CurrentLocale'), merl:term(Context)]).
phrase_locale(Locale, undefined) -> Locale;
phrase_locale(Locale, Context) -> {Locale, Context}.
translated_ast(Text, TreeWalker) ->
translated_ast(Text, undefined, TreeWalker).
translated_ast({noop, Value}, _, TreeWalker) ->
value_ast(Value, true, true, TreeWalker);
translated_ast(Text, {string_literal, _, Context}, TreeWalker) ->
translated_ast(Text, unescape_string_literal(Context), TreeWalker);
translated_ast({string_literal, _, String}, Context, TreeWalker) ->
Text = unescape_string_literal(String),
case call_extension(TreeWalker, translate_ast, [Text, Context, TreeWalker]) of
undefined ->
case TreeWalker#treewalker.context#dtl_context.trans_fun of
none -> runtime_trans_ast(Text, Context, TreeWalker);
Fun when is_function(Fun, 2) ->
compiletime_trans_ast(Fun, Text, Context, TreeWalker);
Fun when is_function(Fun, 1) ->
compiletime_trans_ast(fun (T, _) -> Fun(T) end,
Text, Context, TreeWalker);
Fun ->
empty_ast(?ERR({translation_fun, Fun}, TreeWalker))
end;
TranslatedAst ->
TranslatedAst
end;
translated_ast(Value, Context, TreeWalker) ->
runtime_trans_ast(value_ast(Value, true, false, TreeWalker), Context).
runtime_trans_ast(Text, Context, TreeWalker) ->
Info = #ast_info{ translatable_strings = [Text] },
runtime_trans_ast({{merl:term(Text), Info}, TreeWalker}, Context).
runtime_trans_ast({{ValueAst, AstInfo}, TreeWalker}, undefined) ->
{{?Q("erlydtl_runtime:translate(_@ValueAst, _CurrentLocale, _TranslationFun)"),
AstInfo}, TreeWalker};
runtime_trans_ast({{ValueAst, AstInfo}, TreeWalker}, Context) ->
{{?Q("erlydtl_runtime:translate(_@ValueAst, {_CurrentLocale, _@Context@}, _TranslationFun)"),
AstInfo}, TreeWalker}.
compiletime_trans_ast(TFun, Text, LContext,
#treewalker{
context=#dtl_context{
trans_locales=TLocales
}=Context
}=TreeWalker) ->
ClAst = lists:foldl(
fun(Locale, ClausesAcc) ->
BodyAst = case TFun(Text, phrase_locale(Locale, LContext)) of
default -> string_ast(Text, Context);
Translated -> string_ast(Translated, Context)
end,
Clause = erl_syntax:clause([erl_syntax:abstract(Locale)], none, [BodyAst]),
[Clause | ClausesAcc]
end,
[], TLocales),
{{?Q(["case _CurrentLocale of",
" _@_ClAst -> _;",
" _ -> _@string",
"end"],
[{string, string_ast(Text, Context)}]),
#ast_info{ translatable_strings = [Text] }},
TreeWalker}.
%%% end of context being translation context
%% Completely unnecessary in ErlyDTL (use {{ "{%" }} etc), but implemented for compatibility.
templatetag_ast("openblock", TreeWalker) ->
string_ast("{%", TreeWalker);
templatetag_ast("closeblock", TreeWalker) ->
string_ast("%}", TreeWalker);
templatetag_ast("openvariable", TreeWalker) ->
string_ast("{{", TreeWalker);
templatetag_ast("closevariable", TreeWalker) ->
string_ast("}}", TreeWalker);
templatetag_ast("openbrace", TreeWalker) ->
string_ast("{", TreeWalker);
templatetag_ast("closebrace", TreeWalker) ->
string_ast("}", TreeWalker);
templatetag_ast("opencomment", TreeWalker) ->
string_ast("{#", TreeWalker);
templatetag_ast("closecomment", TreeWalker) ->
string_ast("#}", TreeWalker).
widthratio_ast(Numerator, Denominator, Scale, TreeWalker) ->
{{NumAst, NumInfo}, TreeWalker1} = value_ast(Numerator, false, true, TreeWalker),
{{DenAst, DenInfo}, TreeWalker2} = value_ast(Denominator, false, true, TreeWalker1),
{{ScaleAst, ScaleInfo}, TreeWalker3} = value_ast(Scale, false, true, TreeWalker2),
{{format_number_ast(?Q("erlydtl_runtime:widthratio(_@NumAst, _@DenAst, _@ScaleAst)")),
merge_info(ScaleInfo, merge_info(NumInfo, DenInfo))},
TreeWalker3}.
string_ast(Arg, #treewalker{ context=Context }=TreeWalker) ->
{{string_ast(Arg, Context), #ast_info{}}, TreeWalker};
string_ast(Arg, Context) ->
merl:term(erlydtl_compiler_utils:to_string(Arg, Context)).
include_ast(File, ArgList, Scopes, #treewalker{ context=Context }=TreeWalker) ->
FilePath = full_path(File, Context#dtl_context.doc_root),
?LOG_TRACE("include file: ~s~n", [FilePath], Context),
case parse_file(FilePath, Context) of
{ok, InclusionParseTree, CheckSum} ->
{NewScope, {ArgInfo, TreeWalker1}}
= lists:mapfoldl(
fun ({{identifier, _, LocalVarName}, Value}, {AstInfoAcc, TreeWalkerAcc}) ->
{{Ast, Info}, TW} = value_ast(Value, false, false, TreeWalkerAcc),
{{LocalVarName, Ast}, {merge_info(AstInfoAcc, Info), TW}}
end, {#ast_info{}, TreeWalker}, ArgList),
C = TreeWalker1#treewalker.context,
{{BodyAst, BodyInfo}, TreeWalker2} = with_dependency(
{FilePath, CheckSum},
body_ast(
InclusionParseTree,
TreeWalker1#treewalker{
context=C#dtl_context{
parse_trail = [FilePath | C#dtl_context.parse_trail],
local_scopes = [NewScope|Scopes]
}
})),
{{BodyAst, merge_info(BodyInfo, ArgInfo)},
reset_parse_trail(C#dtl_context.parse_trail, TreeWalker2)};
{error, Reason} ->
empty_ast(?ERR(Reason, TreeWalker))
end.
%% include at run-time
ssi_ast(FileName, #treewalker{
context=#dtl_context{
reader = {Mod, Fun},
reader_options = ReaderOptions,
doc_root = Dir
}
}=TreeWalker) ->
{{FileAst, Info}, TreeWalker1} = value_ast(FileName, true, true, TreeWalker),
{{?Q("erlydtl_runtime:read_file(_@Mod@, _@Fun@, _@Dir@, _@FileAst, _@ReaderOptions@)"), Info}, TreeWalker1}.
filter_tag_ast(FilterList, Contents, TreeWalker) ->
{{InnerAst, Info}, TreeWalker1} = body_ast(Contents, push_auto_escape(did, TreeWalker)),
{{FilteredAst, FilteredInfo}, TreeWalker2} =
lists:foldl(
fun ({{identifier, _, Name}, []}, {{AstAcc, InfoAcc}, TreeWalkerAcc})
when Name =:= 'escape'; Name =:= 'safe'; Name =:= 'safeseq' ->
{{AstAcc, InfoAcc}, TreeWalkerAcc#treewalker{ safe = true }};
(Filter, {{AstAcc, InfoAcc}, TreeWalkerAcc}) ->
{{Ast, AstInfo}, TW} = filter_ast1(Filter, AstAcc, TreeWalkerAcc),
{{Ast, merge_info(InfoAcc, AstInfo)}, TW}
end,
{{?Q("erlang:iolist_to_binary(_@InnerAst)"), Info},
pop_auto_escape(TreeWalker1)},
FilterList),
EscapedAst = case search_for_escape_filter(
lists:reverse(FilterList),
TreeWalker2#treewalker.context) of
on -> ?Q("erlydtl_filters:force_escape(_@FilteredAst)");
_ -> FilteredAst
end,
{{EscapedAst, FilteredInfo}, TreeWalker2}.
search_for_escape_filter(FilterList, #dtl_context{auto_escape = [on|_]}) ->
search_for_safe_filter(FilterList);
search_for_escape_filter(_, #dtl_context{auto_escape = [did|_]}) -> off;
search_for_escape_filter([{{identifier, _, 'escape'}, []}|Rest], _Context) ->
search_for_safe_filter(Rest);
search_for_escape_filter([_|Rest], Context) ->
search_for_escape_filter(Rest, Context);
search_for_escape_filter([], _Context) -> off.
search_for_safe_filter([{{identifier, _, Name}, []}|_])
when Name =:= 'safe'; Name =:= 'safeseq' -> off;
search_for_safe_filter([_|Rest]) -> search_for_safe_filter(Rest);
search_for_safe_filter([]) -> on.
filter_ast(Variable, Filter, TreeWalker) ->
%% the escape filter is special; it is always applied last, so we have to go digging for it
%% AutoEscape = 'did' means we (will have) decided whether to escape the current variable,
%% so don't do any more escaping
{{UnescapedAst, Info}, TreeWalker1} =
filter_ast_noescape(
Variable, Filter,
push_auto_escape(did, TreeWalker)),
{EscapedAst, TreeWalker2} =
case search_for_escape_filter(Variable, Filter, TreeWalker#treewalker.context) of
on -> {?Q("erlydtl_filters:force_escape(_@UnescapedAst)"),
TreeWalker1#treewalker{ safe = true }};
_ -> {UnescapedAst, TreeWalker1}
end,
{{EscapedAst, Info}, pop_auto_escape(TreeWalker2)}.
filter_ast_noescape(Variable, {{identifier, _, Name}, []}, TreeWalker)
when Name =:= 'escape'; Name =:= 'safe'; Name =:= 'safeseq' ->
value_ast(Variable, true, false, TreeWalker#treewalker{safe = true});
filter_ast_noescape(Variable, Filter, TreeWalker) ->
{{ValueAst, Info1}, TreeWalker2} = value_ast(Variable, true, false, TreeWalker),
{{VarValue, Info2}, TreeWalker3} = filter_ast1(Filter, ValueAst, TreeWalker2),
{{VarValue, merge_info(Info1, Info2)}, TreeWalker3}.
filter_ast1({{identifier, Pos, Name}, Args}, ValueAst, TreeWalker) ->
{{ArgsAst, ArgsInfo}, TreeWalker1} =
lists:foldr(
fun (Arg, {{AccAst, AccInfo}, AccTreeWalker}) ->
{{ArgAst, ArgInfo}, ArgTreeWalker} = value_ast(Arg, false, false, AccTreeWalker),
{{[ArgAst|AccAst], merge_info(ArgInfo, AccInfo)}, ArgTreeWalker}
end,
{{[], #ast_info{}}, TreeWalker},
Args),
case filter_ast2(Name, [ValueAst|ArgsAst], TreeWalker1#treewalker.context) of
{ok, FilterAst} ->
{{FilterAst, ArgsInfo}, TreeWalker1};
Error ->
empty_ast(?WARN({Pos, Error}, TreeWalker1))
end.
% special case for date, which reqires localisation
% may be replaced later with a query to a list
% of functions which require translation
filter_ast2('date' = Name, Args, #dtl_context{ filters = Filters } = Ctx) ->
case proplists:get_value(Name, Filters) of
{Mod, Fun} ->
case erlang:function_exported(Mod, Fun, length(Args) + 2) of
true -> {ok, ?Q("'@Mod@':'@Fun@'(_@Args, _TranslationFun, _CurrentLocale )")};
false -> filter_ast3(Name, Args, Ctx) % redefined 'date'?
end;
% should never happen
undefined -> {unknown_filter, Name, length(Args)}
end;
filter_ast2(Name, Args, Ctx) ->
filter_ast3(Name, Args, Ctx).
filter_ast3(Name, Args, #dtl_context{ filters = Filters }) ->
case proplists:get_value(Name, Filters) of
{Mod, Fun}=Filter ->
case erlang:function_exported(Mod, Fun, length(Args)) of
true -> {ok, ?Q("'@Mod@':'@Fun@'(_@Args)")};
false -> {filter_args, Name, Filter, length(Args)}
end;
undefined ->
{unknown_filter, Name, length(Args)}
end.
search_for_escape_filter(Variable, Filter, #dtl_context{auto_escape = [on|_]}) ->
search_for_safe_filter(Variable, Filter);
search_for_escape_filter(_, _, #dtl_context{auto_escape = [did|_]}) ->
off;
search_for_escape_filter(Variable, {{identifier, _, 'escape'}, []} = Filter, _Context) ->
search_for_safe_filter(Variable, Filter);
search_for_escape_filter({apply_filter, Variable, Filter}, _, Context) ->
search_for_escape_filter(Variable, Filter, Context);
search_for_escape_filter(_Variable, _Filter, _Context) ->
off.
search_for_safe_filter(_, {{identifier, _, 'safe'}, []}) ->
off;
search_for_safe_filter(_, {{identifier, _, 'safeseq'}, []}) ->
off;
search_for_safe_filter({apply_filter, Variable, Filter}, _) ->
search_for_safe_filter(Variable, Filter);
search_for_safe_filter(_Variable, _Filter) ->
on.
finder_function(true) -> {erlydtl_runtime, fetch_value};
finder_function(false) -> {erlydtl_runtime, find_value}.
finder_function(EmptyIfUndefined, TreeWalker) ->
case call_extension(TreeWalker, finder_function, [EmptyIfUndefined]) of
undefined -> finder_function(EmptyIfUndefined);
Result -> Result
end.
resolve_variable_ast({extension, Tag}, _, TreeWalker) ->
extension_ast(Tag, TreeWalker);
resolve_variable_ast(VarTuple, EmptyIfUndefined, TreeWalker)
when is_boolean(EmptyIfUndefined) ->
resolve_variable_ast(VarTuple, finder_function(EmptyIfUndefined, TreeWalker), TreeWalker);
resolve_variable_ast(VarTuple, FinderFunction, TreeWalker) ->
resolve_variable_ast1(VarTuple, FinderFunction, TreeWalker).
resolve_variable_ast1({attribute, {{_, Pos, Attr}, Variable}}, {Runtime, Finder}=FinderFunction, TreeWalker) ->
{{VarAst, VarInfo}, TreeWalker1} = resolve_variable_ast(Variable, FinderFunction, TreeWalker),
#treewalker{
context=#dtl_context{
lists_0_based = Lists0Based,
tuples_0_based = Tuples0Based
}
} = TreeWalker,
FileName = get_current_file(TreeWalker1),
{{?Q(["'@Runtime@':'@Finder@'(",
" _@Attr@, _@VarAst,",
" [{lists_0_based, _@Lists0Based@},",
" {tuples_0_based, _@Tuples0Based@},",
" {render_options, RenderOptions},",
" {record_info, _RecordInfo},",
" {filename, _@FileName@},",
" {pos, _@Pos@}",
" ])"]),
VarInfo},
TreeWalker1};
resolve_variable_ast1({variable, {identifier, Pos, VarName}}, {Runtime, Finder}, TreeWalker) ->
{Source, Value, Filters} = resolve_variable(VarName, TreeWalker),
Ast = case {Source, Value} of
{_, undefined} ->
FileName = get_current_file(TreeWalker),
{?Q(["'@Runtime@':'@Finder@'(",
" _@VarName@, _Variables,",
" [{filename, _@FileName@},",
" {pos, _@Pos@},",
" {record_info, _RecordInfo},",
" {render_options, RenderOptions}])"
]),
#ast_info{ var_names=[VarName] }};
{default_vars, Val} ->
FileName = get_current_file(TreeWalker),
{?Q(["'@Runtime@':fetch_value(",
" _@VarName@, _Variables,",
" [{filename, _@FileName@},",
" {pos, _@Pos@},",
" {record_info, _RecordInfo},",
" {render_options, RenderOptions}],",
" _@val)"
],
[{val, merl:term(erlydtl_filters:format_number(Val))}]),
#ast_info{ var_names=[VarName], def_names=[VarName] }};
{constant, Val} ->
{merl:term(erlydtl_filters:format_number(Val)),
#ast_info{ const_names=[VarName] }};
{scope, Val} ->
{Val, #ast_info{}}
end,
lists:foldr(
fun ({escape, []}, {{AccAst, AccInfo}, TW}) ->
{{?Q("erlydtl_filters:force_escape(_@AccAst)"), AccInfo}, TW#treewalker{ safe = true }};
({Safe, []}, {Acc, TW}) when Safe == safe; Safe == safeseq ->
{Acc, TW#treewalker{ safe = true }};
({Filter, Args}, {{AccAst, AccInfo}, TW})
when is_atom(Filter), is_list(Args) ->
case filter_ast2(Filter, [AccAst|Args], TW#treewalker.context) of
{ok, FilteredAst} ->
{{FilteredAst, AccInfo}, TW};
Error ->
empty_ast(?WARN({Pos, Error}, TW))
end
end,
{Ast, TreeWalker},
Filters
).
resolve_reserved_variable(ReservedName, TreeWalker) ->
resolve_reserved_variable(ReservedName, merl:term(undefined), TreeWalker).
resolve_reserved_variable(ReservedName, Default, TreeWalker) ->
case resolve_variable(ReservedName, Default, TreeWalker) of
{Src, Value, []} when Src =:= scope; Value =:= Default ->
{Value, TreeWalker};
_ ->
{Default, ?ERR({reserved_variable, ReservedName}, TreeWalker)}
end.
format({{Ast, Info}, TreeWalker}) ->
auto_escape({{format_number_ast(Ast), Info}, TreeWalker}).
format_number_ast(Ast) ->
?Q("erlydtl_filters:format_number(_@Ast)").
auto_escape({AstInfo, #treewalker{ safe = true }=TW}) ->
{AstInfo, TW#treewalker{ safe = false }};
auto_escape({{Value, Info}, #treewalker{ context=#dtl_context{auto_escape=[on|_]} }=TW}) ->
{{?Q("erlydtl_filters:force_escape(_@Value)"), Info}, TW};
auto_escape(Value) -> Value.
firstof_ast(Vars, TreeWalker) ->
body_ast(
[lists:foldr(
fun ({L, _, _}=Var, [])
when L=:=string_literal;L=:=number_literal ->
Var;
({L, _, _}, _)
when L=:=string_literal;L=:=number_literal ->
erlang:error(errbadliteral);
(Var, []) ->
{'ifelse', Var, [Var], []};
(Var, Acc) ->
{'ifelse', Var, [Var], [Acc]}
end,
[], Vars)
],
TreeWalker).
ifelse_ast(Expression, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, TreeWalker) ->
Info = merge_info(IfContentsInfo, ElseContentsInfo),
{{Ast, ExpressionInfo}, TreeWalker1} = value_ast(Expression, false, false, TreeWalker),
{{?Q(["case erlydtl_runtime:is_true(_@Ast) of",
" true -> _@IfContentsAst;",
" _ -> _@ElseContentsAst",
"end"]),
merge_info(ExpressionInfo, Info)},
TreeWalker1}.
with_ast(ArgList, Contents, TreeWalker) ->
{ArgAstList, {ArgInfo, TreeWalker1}} =
lists:mapfoldl(
fun ({{identifier, _, _LocalVarName}, Value}, {AstInfoAcc, TreeWalkerAcc}) ->
{{Ast, Info}, TW} = value_ast(Value, false, false, TreeWalkerAcc),
{Ast, {merge_info(AstInfoAcc, Info), TW}}
end,
{#ast_info{}, TreeWalker},
ArgList),
NewScope = lists:map(
fun ({{identifier, _, LocalVarName}, _Value}) ->
{LocalVarName, varname_ast(LocalVarName)}
end,
ArgList),
{{InnerAst, InnerInfo}, TreeWalker2} =
body_ast(
Contents,
push_scope(NewScope, TreeWalker1)),
{{?Q("fun (_@args) -> _@InnerAst end (_@ArgAstList)",
[{args, element(2, lists:unzip(NewScope))}]),
merge_info(ArgInfo, InnerInfo)},
restore_scope(TreeWalker1, TreeWalker2)}.
scope_as(VarName, Contents, TreeWalker) ->
{{ContentsAst, ContentsInfo}, TreeWalker1} = body_ast(Contents, TreeWalker),
VarAst = varname_ast(VarName),
{Id, TreeWalker2} = begin_scope(
{[{VarName, VarAst}],
[?Q("_@VarAst = _@ContentsAst")]},
TreeWalker1),
{{Id, ContentsInfo}, TreeWalker2}.
regroup_ast(ListVariable, GrouperVariable, LocalVarName, TreeWalker) ->
{{ListAst, ListInfo}, TreeWalker1} = value_ast(ListVariable, false, true, TreeWalker),
LocalVarAst = varname_ast(LocalVarName),
{Id, TreeWalker2} = begin_scope(
{[{LocalVarName, LocalVarAst}],
[?Q(["_@LocalVarAst = erlydtl_runtime:regroup(",
" _@ListAst, _@regroup,",
" [{record_info, _RecordInfo}]",
")"],
[{regroup, regroup_filter(GrouperVariable, [])}])
]},
TreeWalker1),
{{Id, ListInfo}, TreeWalker2}.
regroup_filter({attribute,{{identifier,_,Ident},Next}},Acc) ->
regroup_filter(Next,[erl_syntax:atom(Ident)|Acc]);
regroup_filter({variable,{identifier,_,Var}},Acc) ->
erl_syntax:list([erl_syntax:atom(Var)|Acc]).
to_list_ast(Value, IsReversed) ->
?Q("erlydtl_runtime:to_list(_@Value, _@IsReversed)").
to_list_ast(Value, IsReversed, TreeWalker) ->
case call_extension(TreeWalker, to_list_ast, [Value, IsReversed, TreeWalker]) of
undefined -> to_list_ast(Value, IsReversed);
Result -> Result
end.
for_loop_ast(IteratorList, LoopValue, IsReversed, Contents,
{EmptyContentsAst, EmptyContentsInfo},
#treewalker{ context=Context }=TreeWalker) ->
%% create unique namespace for this instance
Level = length(Context#dtl_context.local_scopes),
{Row, Col} = element(2, hd(IteratorList)),
ForId = lists:concat(["/", Level, "_", Row, ":", Col]),
Counters = merl:var(lists:concat(["Counters", ForId])),
Vars = merl:var(lists:concat(["Vars", ForId])),
%% setup
VarScope = lists:map(
fun({identifier, {R, C}, Iterator}) ->
{Iterator, varname_ast(lists:concat([
Iterator,"/", Level, "_", R, ":", C]))}
end, IteratorList),
{Iterators, IteratorVars} = lists:unzip(VarScope),
IteratorCount = length(IteratorVars),
{{LoopBodyAst, Info}, TreeWalker1} =
body_ast(
Contents,
push_scope([{'forloop', Counters} | VarScope],
TreeWalker)),
{{LoopValueAst, LoopValueInfo}, TreeWalker2} =
value_ast(LoopValue, false, true, restore_scope(TreeWalker, TreeWalker1)),
LoopValueAst0 = to_list_ast(LoopValueAst, merl:term(IsReversed), TreeWalker2),
{ParentLoop, TreeWalker3} = resolve_reserved_variable('forloop', TreeWalker2),
{{?Q(["erlydtl_runtime:forloop(",
" fun (_@Vars, _@Counters) ->",
" {_@IteratorVars} = if is_tuple(_@Vars), size(_@Vars) == _@IteratorCount@ -> _@Vars;",
" _@___ifclauses -> _",
" end,",
" {_@LoopBodyAst, erlydtl_runtime:increment_counter_stats(_@Counters)}",
" end,",
" _@LoopValueAst0, _@ParentLoop, _@EmptyContentsAst)"],
[{ifclauses, if IteratorCount > 1 ->
?Q(["() when is_list(_@Vars), length(_@Vars) == _@IteratorCount@ ->",
" list_to_tuple(_@Vars);",
"() when true -> throw({for_loop, _@Iterators@, _@Vars})"]);
true ->
?Q("() when true -> {_@Vars}")
end}]),
merge_info(merge_info(Info, EmptyContentsInfo), LoopValueInfo)},
TreeWalker3}.
ifchanged_values_ast(Values, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, TreeWalker) ->
Info = merge_info(IfContentsInfo, ElseContentsInfo),
ValueAstFun = fun (Expr, {LTreeWalker, LInfo, Acc}) ->
{{EAst, EInfo}, ETw} = value_ast(Expr, false, true, LTreeWalker),
{ETw, merge_info(LInfo, EInfo),
[?Q("{_@hash, _@EAst}",
[{hash, merl:term(erlang:phash2(Expr))}])
|Acc]}
end,
{TreeWalker1, MergedInfo, Changed} = lists:foldl(ValueAstFun, {TreeWalker, Info, []}, Values),
{{?Q(["case erlydtl_runtime:ifchanged([_@Changed]) of",
" true -> _@IfContentsAst;",
" _ -> _@ElseContentsAst",
"end"]),
MergedInfo},
TreeWalker1}.
ifchanged_contents_ast(Contents, {IfContentsAst, IfContentsInfo}, {ElseContentsAst, ElseContentsInfo}, TreeWalker) ->
{{?Q(["case erlydtl_runtime:ifchanged([{_@hash, _@IfContentsAst}]) of",
" true -> _@IfContentsAst;",
" _ -> _@ElseContentsAst",
"end"],
[{hash, merl:term(erlang:phash2(Contents))}]),
merge_info(IfContentsInfo, ElseContentsInfo)},
TreeWalker}.
cycle_ast(Names, undefined, #treewalker{ context=Context }=TreeWalker) ->
{NamesTuple, VarNames}
= lists:mapfoldl(
fun ({string_literal, _, Str}, VarNamesAcc) ->
S = string_ast(unescape_string_literal(Str), Context),
{S, VarNamesAcc};
({variable, _}=Var, VarNamesAcc) ->
{{V, #ast_info{ var_names=[VarName] }}, _} = resolve_variable_ast(Var, true, TreeWalker),
{V, [VarName|VarNamesAcc]};
({number_literal, _, Num}, VarNamesAcc) ->
{format_number_ast(erl_syntax:integer(Num)), VarNamesAcc};
(_, VarNamesAcc) ->
{[], VarNamesAcc}
end, [], Names),
{ForLoop, TreeWalker1} = resolve_reserved_variable('forloop', TreeWalker),
{{?Q("erlydtl_runtime:cycle({_@NamesTuple}, _@ForLoop)"),
#ast_info{ var_names = VarNames }},
TreeWalker1};
cycle_ast(Names, [{identifier, _, VarName}|Opts], TreeWalker) ->
{{VarAst, AstInfo}, TW1} = cycle_ast(Names, undefined, TreeWalker),
VarNameAst = varname_ast(VarName),
{Scope, TW2} = begin_scope(
{[{VarName, VarNameAst}],
[?Q("_@VarNameAst = _@VarAst")
| case Opts of
[silent] -> [];
[] -> [VarAst]
end
]},
TW1),
{{Scope, AstInfo}, TW2}.
%% Older Django templates treat cycle with comma-delimited elements as strings
cycle_compat_ast(Names, #treewalker{ context=Context }=TreeWalker) ->
NamesTuple = lists:map(
fun ({identifier, _, X}) ->
string_ast(X, Context)
end, Names),
{ForLoop, TreeWalker1} = resolve_reserved_variable('forloop', TreeWalker),
{{?Q("erlydtl_runtime:cycle({_@NamesTuple}, _@ForLoop)"),
#ast_info{}},
TreeWalker1}.
now_ast(FormatString, TreeWalker) ->
%% Note: we can't use unescape_string_literal here
%% because we want to allow escaping in the format string.
%% We only want to remove the surrounding escapes,
%% i.e. \"foo\" becomes "foo"
UnescapeOuter = string:strip(FormatString, both, 34),
{{StringAst, Info}, TreeWalker1} = string_ast(UnescapeOuter, TreeWalker),
{{?Q("erlydtl_dateformat:format(_@StringAst, _TranslationFun, _CurrentLocale)"), Info}, TreeWalker1}.
spaceless_ast(Contents, TreeWalker) ->
{{Ast, Info}, TreeWalker1} = body_ast(Contents, TreeWalker),
{{?Q("erlydtl_runtime:spaceless(_@Ast)"), Info}, TreeWalker1}.
load_libs_ast(Libs, TreeWalker) ->
TreeWalker1 = lists:foldl(
fun ({identifier, Pos, Lib}, TW) ->
load_library(Pos, Lib, TW)
end,
TreeWalker, Libs),
empty_ast(TreeWalker1).
load_from_lib_ast(What, {identifier, Pos, Lib}, TreeWalker) ->
Names = lists:foldl(
fun ({identifier, _, Name}, Acc) -> [Name|Acc] end,
[], What),
empty_ast(load_library(Pos, Lib, Names, TreeWalker)).
%%-------------------------------------------------------------------
%% Custom tags
%%-------------------------------------------------------------------
interpret_value({trans, StringLiteral}, TreeWalker) ->
translated_ast(StringLiteral, TreeWalker);
interpret_value(Value, TreeWalker) ->
value_ast(Value, false, false, TreeWalker).
interpret_args(Args, TreeWalker) ->
lists:foldr(
fun ({{identifier, _, Key}, Value}, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) ->
{{Ast0, AstInfo0}, TreeWalker0} = interpret_value(Value, TreeWalkerAcc),
{{[?Q("{_@Key@, _@Ast0}")|ArgsAcc], merge_info(AstInfo0, AstInfoAcc)}, TreeWalker0};
(Value, {{ArgsAcc, AstInfoAcc}, TreeWalkerAcc}) ->
{{Ast0, AstInfo0}, TreeWalker0} = value_ast(Value, false, false, TreeWalkerAcc),
{{[Ast0|ArgsAcc], merge_info(AstInfo0, AstInfoAcc)}, TreeWalker0}
end, {{[], #ast_info{}}, TreeWalker}, Args).
tag_ast(Name, Args, TreeWalker) ->
{{InterpretedArgs, AstInfo1}, TreeWalker1} = interpret_args(Args, TreeWalker),
{{RenderAst, RenderInfo}, TreeWalker2} = custom_tags_modules_ast(Name, InterpretedArgs, TreeWalker1),
{{RenderAst, merge_info(AstInfo1, RenderInfo)}, TreeWalker2}.
custom_tags_modules_ast({identifier, Pos, Name}, InterpretedArgs,
#treewalker{
context=#dtl_context{
tags = Tags,
module = Module,
is_compiling_dir=IsCompilingDir
}
}=TreeWalker) ->
case proplists:get_value(Name, Tags) of
{Mod, Fun}=Tag ->
case lists:max([-1] ++ [I || {N,I} <- Mod:module_info(exports), N =:= Fun]) of
2 ->
{{?Q("'@Mod@':'@Fun@'([_@InterpretedArgs], RenderOptions)"),
#ast_info{}}, TreeWalker};
1 ->
{{?Q("'@Mod@':'@Fun@'([_@InterpretedArgs])"),
#ast_info{}}, TreeWalker};
-1 ->
empty_ast(?WARN({Pos, {missing_tag, Name, Tag}}, TreeWalker));
I ->
empty_ast(?WARN({Pos, {bad_tag, Name, Tag, I}}, TreeWalker))
end;
undefined ->
if IsCompilingDir =/= false ->
{{?Q("'@Module@':'@Name@'([_@InterpretedArgs], RenderOptions)"),
#ast_info{ custom_tags = [Name] }}, TreeWalker};
true ->
{{?Q("render_tag(_@Name@, [_@InterpretedArgs], RenderOptions)"),
#ast_info{ custom_tags = [Name] }}, TreeWalker}
end
end.
tag_ast(Name, Args, NewTagVar, TreeWalker) ->
{{InterpretedArgs, AstInfo1}, TreeWalker1} = interpret_args(Args, TreeWalker),
{{RenderAst, RenderInfo}, TreeWalker2} = custom_tags_modules_ast(Name, InterpretedArgs, NewTagVar, TreeWalker1),
{{RenderAst, merge_info(AstInfo1, RenderInfo)}, TreeWalker2}.
custom_tags_modules_ast({identifier, Pos, Name}, InterpretedArgs, NewTagVar,
#treewalker{
context=#dtl_context{
tags = Tags,
module = Module,
is_compiling_dir=IsCompilingDir
}
}=TreeWalker) ->
LocalVarAst = varname_ast(NewTagVar),
case proplists:get_value(Name, Tags) of
{Mod, Fun}=Tag ->
case lists:max([-1] ++ [I || {N,I} <- Mod:module_info(exports), N =:= Fun]) of
2 ->
{Id, TreeWalker1} = begin_scope(
{[{NewTagVar, LocalVarAst}],
[?Q("_@LocalVarAst = '@Mod@':'@Fun@'([_@InterpretedArgs], RenderOptions)")]},
TreeWalker
),
{{Id, #ast_info{}}, TreeWalker1};
1 ->
{Id, TreeWalker1} = begin_scope(
{[{NewTagVar, LocalVarAst}],
[?Q("_@LocalVarAst = '@Mod@':'@Fun@'([_@InterpretedArgs])")]},
TreeWalker
),
{{Id, #ast_info{}}, TreeWalker1};
-1 ->
empty_ast(?WARN({Pos, {missing_tag, Name, Tag}}, TreeWalker));
I ->
empty_ast(?WARN({Pos, {bad_tag, Name, Tag, I}}, TreeWalker))
end;
undefined ->
if IsCompilingDir =/= false ->
{Id, TreeWalker1} = begin_scope(
{[{NewTagVar, LocalVarAst}],
[?Q("_@LocalVarAst = '@Module@':'@Name@'([_@InterpretedArgs], RenderOptions)")]},
TreeWalker
),
{{Id, #ast_info{ custom_tags = [Name]}}, TreeWalker1};
true ->
{Id, TreeWalker1} = begin_scope(
{[{NewTagVar, LocalVarAst}],
[?Q("_@LocalVarAst = '@Module@':'@Name@'([_@InterpretedArgs], RenderOptions)")]},
TreeWalker
),
{{Id, #ast_info{ custom_tags = [Name]}}, TreeWalker1}
end
end.
call_ast(Module, TreeWalker) ->
call_ast(Module, merl:var("_Variables"), #ast_info{}, TreeWalker).
call_with_ast(Module, Variable, TreeWalker) ->
{{VarAst, VarInfo}, TreeWalker2} = resolve_variable_ast(Variable, false, TreeWalker),
call_ast(Module, VarAst, VarInfo, TreeWalker2).
call_ast(Module, Variable, AstInfo, TreeWalker) ->
Ast = ?Q(["case '@Module@':render(_@Variable, RenderOptions) of",
" {ok, Rendered} -> Rendered;",
" {error, Reason} -> io_lib:format(\"error: ~p\", [Reason])",
"end"]),
with_dependencies(Module:dependencies(), {{Ast, AstInfo}, TreeWalker}).
create_scope(Vars, VarScope) ->
{Scope, Values} =
lists:foldl(
fun (Var, {VarAcc, ValueAcc}) ->
{Name, Value, Filters} =
case Var of
{N, V} -> {N, V, []};
{_, _, _} -> Var
end,
NameAst = varname_ast(lists:concat(["_", Name, VarScope])),
{[{Name, NameAst, Filters}|VarAcc],
[?Q("_@NameAst = _@Value")|ValueAcc]
}
end,
empty_scope(),
Vars),
{Scope, [Values]}.
create_scope(Vars, {Row, Col}, FileName, #treewalker{ context=Context }) ->
Level = length(Context#dtl_context.local_scopes),
create_scope(Vars, lists:concat(["::", FileName, "[", Level, ",", Row, ":", Col, "]"])).
varname_ast([$_|VarName]) ->
merl:var(lists:concat(["_Var__", VarName]));
varname_ast(VarName) ->
merl:var(lists:concat(["Var_", VarName])).
erlydtl-0.15.0/src/erlydtl_compiler.erl 0000664 0000000 0000000 00000045751 15047124314 0020135 0 ustar 00root root 0000000 0000000 %%%-------------------------------------------------------------------
%%% File: erlydtl_compiler.erl
%%% @author Roberto Saccon [http://rsaccon.com]
%%% @author Evan Miller
%%% @author Andreas Stenius
%%% @copyright 2008 Roberto Saccon, Evan Miller
%%% @copyright 2014 Andreas Stenius
%%% @doc
%%% ErlyDTL template compiler
%%% @end
%%%
%%% The MIT License
%%%
%%% Copyright (c) 2007 Roberto Saccon, Evan Miller
%%% Copyright (c) 2014 Andreas Stenius
%%%
%%% 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.
%%%
%%% @since 2007-12-16 by Roberto Saccon, Evan Miller
%%% @since 2014 by Andreas Stenius
%%%-------------------------------------------------------------------
-module(erlydtl_compiler).
-author('rsaccon@gmail.com').
-author('emmiller@gmail.com').
-author('Andreas Stenius ').
%% --------------------------------------------------------------------
%% Definitions
%% --------------------------------------------------------------------
-export([compile_file/3, compile_template/3, compile_dir/3,
format_error/1, default_options/0]).
%% internal use
-export([parse_file/2, parse_template/2, do_parse_template/2]).
-import(erlydtl_compiler_utils,
[add_filters/2, add_tags/2, call_extension/3,
load_library/2, shorten_filename/1, get_current_file/1]).
-include("erlydtl_ext.hrl").
%% --------------------------------------------------------------------
%% API
%% --------------------------------------------------------------------
default_options() -> [verbose, report].
compile_template(Template, Module, Options) ->
Context = process_opts(undefined, Module, Options),
Bin = iolist_to_binary(Template),
?LOG_INFO("Compile template: ~32s~s~n",
[Bin, if size(Bin) > 32 -> "...";
true -> ""
end],
Context),
compile(Context#dtl_context{ bin = Bin }).
compile_file(File, Module, Options) ->
Context = process_opts(File, Module, Options),
?LOG_INFO("Compile file: ~s~n", [File], Context),
compile(Context).
compile_dir(Dir, Module, Options) ->
Context = process_opts({dir, Dir}, Module, Options),
?LOG_INFO("Compile directory: ~s~n", [Dir], Context),
compile(Context).
format_error({read_file, Error}) ->
io_lib:format(
"Failed to read file: ~s",
[file:format_error(Error)]);
format_error({read_file, File, Error}) ->
io_lib:format(
"Failed to include file ~s: ~s",
[File, file:format_error(Error)]);
format_error({deprecated_option, Opt, NewOpt}) ->
io_lib:format(
"Compile option '~s' has been deprecated. Use '~s' instead.",
[Opt, NewOpt]);
format_error(Error) ->
erlydtl_compiler_utils:format_error(Error).
%%====================================================================
%% Internal functions
%%====================================================================
process_opts(File, Module, Options0) ->
Options1 = proplists:normalize(
update_defaults(Options0),
[{aliases, deprecated_opts()}
]),
Source0 = filename:absname(
case File of
undefined ->
filename:join(
[case proplists:get_value(out_dir, Options1, false) of
false -> ".";
OutDir -> OutDir
end,
Module]);
{dir, Dir} ->
Dir;
_ ->
File
end),
Source = shorten_filename(Source0),
Options = [{compiler_options, [{source, Source}]}
|compiler_opts(Options1, [])],
Context =
case File of
{dir, _} ->
init_context([], Source, Module, Options);
_ ->
init_context([Source], filename:dirname(Source), Module, Options)
end,
%% check original options here now that we have a context to
%% process any warnings/errors generated.
check_opts(Options0, Context).
deprecated_opts() ->
[{outdir, out_dir},
{vars, default_vars},
{blocktrans_fun, translation_fun},
{blocktrans_locales, locales}].
check_opts(Options, Context) ->
lists:foldl(
fun ({Opt, NewOpt}, Ctx) ->
case proplists:get_value(Opt, Options) of
undefined -> Ctx;
_ -> ?WARN({deprecated_option, Opt, NewOpt}, Ctx)
end
end, Context, deprecated_opts()).
compiler_opts([CompilerOption|Os], Acc)
when
CompilerOption =:= return;
CompilerOption =:= return_warnings;
CompilerOption =:= return_errors;
CompilerOption =:= report;
CompilerOption =:= report_warnings;
CompilerOption =:= report_errors;
CompilerOption =:= warnings_as_errors;
CompilerOption =:= verbose;
CompilerOption =:= debug_info ->
compiler_opts(Os, [CompilerOption, {compiler_options, [CompilerOption]}|Acc]);
compiler_opts([O|Os], Acc) ->
compiler_opts(Os, [O|Acc]);
compiler_opts([], Acc) ->
lists:reverse(Acc).
update_defaults(Options) ->
maybe_add_env_default_opts(Options).
maybe_add_env_default_opts(Options) ->
case proplists:get_bool(no_env, Options) of
true -> Options;
_ -> Options ++ env_default_opts()
end.
%% shamelessly borrowed from:
%% https://github.com/erlang/otp/blob/21095e6830f37676dd29c33a590851ba2c76499b/\
%% lib/compiler/src/compile.erl#L128
env_default_opts() ->
Key = "ERLYDTL_COMPILER_OPTIONS",
case os:getenv(Key) of
false -> [];
Str when is_list(Str) ->
case erl_scan:string(Str) of
{ok,Tokens,_} ->
case erl_parse:parse_term(Tokens ++ [{dot, 1}]) of
{ok,List} when is_list(List) -> List;
{ok,Term} -> [Term];
{error,_Reason} ->
io:format("Ignoring bad term in ~s\n", [Key]),
[]
end;
{error, {_,_,_Reason}, _} ->
io:format("Ignoring bad term in ~s\n", [Key]),
[]
end
end.
compile(Context) ->
Context1 = do_compile(Context),
collect_result(Context1).
collect_result(#dtl_context{
module=Module,
errors=#error_info{ list=[] },
warnings=Ws }=Context) ->
Info = case Ws of
#error_info{ return=true, list=Warnings } ->
[pack_error_list(Warnings)];
_ ->
[]
end,
Res = case proplists:get_bool(binary, Context#dtl_context.all_options) of
true ->
[ok, Module, Context#dtl_context.bin | Info];
false ->
[ok, Module | Info]
end,
list_to_tuple(Res);
collect_result(#dtl_context{ errors=Es, warnings=Ws }) ->
if Es#error_info.return ->
{error,
pack_error_list(Es#error_info.list),
case Ws of
#error_info{ list=L } ->
pack_error_list(L);
_ ->
[]
end};
true -> error
end.
init_context(ParseTrail, DefDir, Module, Options) when is_list(Module) ->
init_context(ParseTrail, DefDir, list_to_atom(Module), Options);
init_context(ParseTrail, DefDir, Module, Options) ->
Ctx = #dtl_context{},
Locales = lists:usort(
lists:concat(
[proplists:get_all_values(locale, Options),
proplists:get_value(locales, Options, Ctx#dtl_context.trans_locales)]
)),
Context0 =
#dtl_context{
all_options = Options,
auto_escape = case proplists:get_value(auto_escape, Options, true) of
true -> [on];
_ -> [off]
end,
parse_trail = ParseTrail,
module = Module,
doc_root = proplists:get_value(doc_root, Options, DefDir),
libraries = proplists:get_value(libraries, Options, Ctx#dtl_context.libraries),
custom_tags_dir = proplists:get_value(
custom_tags_dir, Options,
filename:join([erlydtl_deps:get_base_dir(), "priv", "custom_tags"])),
trans_fun = erlydtl_runtime:init_translation(
proplists:get_value(translation_fun, Options, Ctx#dtl_context.trans_fun)),
trans_locales = Locales,
vars = proplists:get_value(default_vars, Options, Ctx#dtl_context.vars),
const = proplists:get_value(constants, Options, Ctx#dtl_context.const),
reader = proplists:get_value(reader, Options, Ctx#dtl_context.reader),
reader_options = proplists:get_value(reader_options, Options, Ctx#dtl_context.reader_options),
compiler_options = proplists:append_values(compiler_options, Options),
binary_strings = proplists:get_value(binary_strings, Options, Ctx#dtl_context.binary_strings),
force_recompile = proplists:get_bool(force_recompile, Options),
verbose = length(proplists:get_all_values(verbose, Options)),
is_compiling_dir = if ParseTrail == [] -> DefDir; true -> false end,
extension_module = proplists:get_value(extension_module, Options, Ctx#dtl_context.extension_module),
scanner_module = proplists:get_value(scanner_module, Options, Ctx#dtl_context.scanner_module),
record_info = [{R, lists:zip(I, lists:seq(2, length(I) + 1))}
|| {R, I} <- proplists:get_value(record_info, Options, Ctx#dtl_context.record_info)],
errors = init_error_info(errors, Ctx#dtl_context.errors, Options),
warnings = init_error_info(warnings, Ctx#dtl_context.warnings, Options),
lists_0_based = proplists:get_value(lists_0_based, Options, Ctx#dtl_context.lists_0_based),
tuples_0_based = proplists:get_value(tuples_0_based, Options, Ctx#dtl_context.tuples_0_based),
checks = proplists:substitute_negations(
[{no_non_block_tag, non_block_tag}],
proplists:get_all_values(w, Options))
},
Context1 = load_libraries(proplists:get_value(default_libraries, Options, []), Context0),
Context = default_checks(Ctx#dtl_context.checks, Context1),
case call_extension(Context, init_context, [Context]) of
{ok, C} when is_record(C, dtl_context) -> C;
undefined -> Context
end.
default_checks([], Context) -> Context;
default_checks([C|Cs], #dtl_context{ checks = Checks0 } = Context) ->
Checks =
case proplists:get_value(C, Checks0) of
undefined -> [C|Checks0];
_ -> Checks0
end,
default_checks(Cs, Context#dtl_context{ checks = Checks }).
init_error_info(warnings, Ei, Options) ->
case proplists:get_bool(warnings_as_errors, Options) of
true -> warnings_as_errors;
false ->
init_error_info(get_error_info_opts(warnings, Options), Ei)
end;
init_error_info(Class, Ei, Options) ->
init_error_info(get_error_info_opts(Class, Options), Ei).
init_error_info([{return, true}|Flags], #error_info{ return = false }=Ei) ->
init_error_info(Flags, Ei#error_info{ return = true });
init_error_info([{report, true}|Flags], #error_info{ report = false }=Ei) ->
init_error_info(Flags, Ei#error_info{ report = true });
init_error_info([_|Flags], Ei) ->
init_error_info(Flags, Ei);
init_error_info([], Ei) -> Ei.
get_error_info_opts(Class, Options) ->
Flags = case Class of
errors ->
[return, report, {return_errors, return}, {report_errors, report}];
warnings ->
[return, report, {return_warnings, return}, {report_warnings, report}]
end,
[begin
{Key, Value} = if is_atom(Flag) -> {Flag, Flag};
true -> Flag
end,
{Value, proplists:get_bool(Key, Options)}
end || Flag <- Flags].
load_libraries([], #dtl_context{ all_options=Options }=Context) ->
%% Load filters and tags passed using the old options
Filters = proplists:get_value(custom_filters_modules, Options, []) ++ [erlydtl_filters],
Tags = proplists:get_value(custom_tags_modules, Options, []),
load_legacy_filters(Filters, load_legacy_tags(Tags, Context));
load_libraries([Lib|Libs], Context) ->
load_libraries(Libs, load_library(Lib, Context)).
load_legacy_filters([], Context) -> Context;
load_legacy_filters([Mod|Mods], Context) ->
{Filters, Context1} = read_legacy_library(Mod, Context),
load_legacy_filters(Mods, add_filters(Filters, Context1)).
load_legacy_tags([], Context) -> Context;
load_legacy_tags([Mod|Mods], Context) ->
{Tags, Context1} = read_legacy_library(Mod, Context),
load_legacy_tags(Mods, add_tags(Tags, Context1)).
read_legacy_library(Mod, Context) ->
case code:ensure_loaded(Mod) of
{module, Mod} ->
{[{Name, {Mod, Name}}
|| {Name, _} <- lists:ukeysort(1, Mod:module_info(exports)),
Name =/= module_info
], Context};
{error, Reason} ->
{[], ?WARN({load_library, '(custom-legacy)', Mod, Reason}, Context)}
end.
is_up_to_date(_, #dtl_context{force_recompile = true}) ->
false;
is_up_to_date(CheckSum, Context) ->
erlydtl_beam_compiler:is_up_to_date(CheckSum, Context).
parse_file(File, Context) ->
{M, F} = Context#dtl_context.reader,
ReaderOptions = Context#dtl_context.reader_options,
case catch erlydtl_runtime:read_file_internal(M, F, File, ReaderOptions) of
{ok, Data} ->
parse_template(Data, Context);
{error, Reason} ->
{error, {read_file, File, Reason}}
end.
parse_template(Data, Context) ->
CheckSum = binary_to_list(erlang:md5(Data)),
case is_up_to_date(CheckSum, Context) of
true -> up_to_date;
false ->
case do_parse_template(Data, Context) of
{ok, Val} -> {ok, Val, CheckSum};
Err -> Err
end
end.
do_parse_template(Data, #dtl_context{ scanner_module=Scanner }=Context) ->
check_scan(
apply(Scanner, scan, [Data]),
Context).
check_scan({ok, Tokens}, Context) ->
Tokens1 = case call_extension(Context, post_scan, [Tokens]) of
undefined -> Tokens;
{ok, T} -> T
end,
check_parse(erlydtl_parser:parse(Tokens1), [], Context#dtl_context{ scanned_tokens=Tokens1 });
check_scan({error, Err, State}, Context) ->
case call_extension(Context, scan, [State]) of
undefined ->
{error, Err};
{ok, NewState} ->
check_scan(apply(Context#dtl_context.scanner_module, resume, [NewState]), Context);
ExtRes ->
ExtRes
end;
check_scan({error, _}=Error, _Context) ->
Error.
check_parse({ok, _}=Ok, [], _Context) -> Ok;
check_parse({ok, Parsed}, Acc, _Context) -> {ok, Acc ++ Parsed};
check_parse({error, _}=Err, _, _Context) -> Err;
check_parse({error, Err, State}, Acc, Context) ->
{State1, Parsed} = reset_parse_state(State, Context),
case call_extension(Context, parse, [State1]) of
undefined ->
{error, Err};
{ok, ExtParsed} ->
{ok, Acc ++ Parsed ++ ExtParsed};
{error, ExtErr, ExtState} ->
case reset_parse_state(ExtState, Context) of
{_, []} ->
%% todo: see if this is indeed a sensible ext error,
%% or if we should rather present the original Err message
{error, ExtErr};
{State2, ExtParsed} ->
check_parse(erlydtl_parser:resume(State2), Acc ++ Parsed ++ ExtParsed, Context)
end;
ExtRes ->
ExtRes
end.
%% backtrack up to the nearest opening tag, and keep the value stack parsed ok so far
reset_parse_state([[{Tag, _, _}|_]=Ts, Tzr, _, _, Stack], Context)
when Tag==open_tag; Tag==open_var ->
%% reached opening tag, so the stack should be sensible here
{[reset_token_stream(Ts, Context#dtl_context.scanned_tokens),
Tzr, 0, [], []], lists:flatten(Stack)};
reset_parse_state([_, _, 0, [], []]=State, _Context) ->
%% top of (empty) stack
{State, []};
reset_parse_state([Ts, Tzr, _, [0 | []], [Parsed | []]], Context)
when is_list(Parsed) ->
%% top of good stack
{[reset_token_stream(Ts, Context#dtl_context.scanned_tokens),
Tzr, 0, [], []], Parsed};
reset_parse_state([Ts, Tzr, _, [S | Ss], [T | Stack]], Context) ->
%% backtrack...
reset_parse_state([[T|Ts], Tzr, S, Ss, Stack], Context).
reset_token_stream([T|_], [T|Ts]) -> [T|Ts];
reset_token_stream(Ts, [_|S]) ->
reset_token_stream(Ts, S).
%% we should find the next token in the list of scanned tokens, or something is real fishy
pack_error_list(Es) ->
collect_error_info([], Es, []).
collect_error_info([], [], Acc) ->
lists:reverse(Acc);
collect_error_info([{File, ErrorInfo}|Es], Rest, [{File, FEs}|Acc]) ->
collect_error_info(Es, Rest, [{File, ErrorInfo ++ FEs}|Acc]);
collect_error_info([E|Es], Rest, Acc) ->
collect_error_info(Es, [E|Rest], Acc);
collect_error_info([], Rest, Acc) ->
case lists:reverse(Rest) of
[E|Es] ->
collect_error_info(Es, [], [E|Acc])
end.
do_compile(#dtl_context{ is_compiling_dir=false, bin=undefined }=Context) ->
compile_output(parse_file(get_current_file(Context), Context), Context);
do_compile(#dtl_context{ is_compiling_dir=false, bin=Template }=Context) ->
compile_output(parse_template(Template, Context), Context);
do_compile(#dtl_context{ is_compiling_dir=Dir }=Context) ->
erlydtl_beam_compiler:compile_dir(Dir, Context).
compile_output(up_to_date, Context) -> Context;
compile_output({ok, DjangoParseTree, CheckSum}, Context) ->
erlydtl_beam_compiler:compile(DjangoParseTree, CheckSum, Context#dtl_context{ bin=undefined });
compile_output({error, Reason}, Context) -> ?ERR(Reason, Context).
erlydtl-0.15.0/src/erlydtl_compiler_utils.erl 0000664 0000000 0000000 00000047330 15047124314 0021350 0 ustar 00root root 0000000 0000000 %%%-------------------------------------------------------------------
%%% File: erlydtl_compiler_utils.erl
%%% @author Roberto Saccon [http://rsaccon.com]
%%% @author Evan Miller
%%% @author Andreas Stenius
%%% @copyright 2008 Roberto Saccon, Evan Miller
%%% @copyright 2014 Andreas Stenius
%%% @doc
%%% ErlyDTL template compiler utils.
%%% @end
%%%
%%% The MIT License
%%%
%%% Copyright (c) 2007 Roberto Saccon, Evan Miller
%%% Copyright (c) 2014 Andreas Stenius
%%%
%%% 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.
%%%
%%% @since 2007-12-16 by Roberto Saccon, Evan Miller
%%% @since 2014 by Andreas Stenius
%%%-------------------------------------------------------------------
-module(erlydtl_compiler_utils).
-author('rsaccon@gmail.com').
-author('emmiller@gmail.com').
-author('Andreas Stenius ').
%% --------------------------------------------------------------------
%% Definitions
%% --------------------------------------------------------------------
-define(LIB_VERSION, 1).
-export([
add_error/3, add_errors/2,
add_filters/2,
add_tags/2,
add_warning/3, add_warnings/2,
begin_scope/1, begin_scope/2,
call_extension/3,
empty_scope/0,
end_scope/4,
format_error/1,
full_path/2,
get_current_file/1,
init_treewalker/1,
is_stripped_token_empty/1,
load_library/2, load_library/3, load_library/4,
merge_info/2,
print/3, print/4,
push_scope/2,
reset_block_dict/2,
reset_parse_trail/2,
resolve_variable/2, resolve_variable/3,
restore_scope/2,
shorten_filename/1, shorten_filename/2,
to_string/2,
unescape_string_literal/1,
push_auto_escape/2,
pop_auto_escape/1,
token_pos/1
]).
-include("erlydtl_ext.hrl").
%% --------------------------------------------------------------------
%% API
%% --------------------------------------------------------------------
init_treewalker(Context) ->
TreeWalker = #treewalker{ context=Context },
case call_extension(Context, init_treewalker, [TreeWalker]) of
{ok, TW} when is_record(TW, treewalker) -> TW;
undefined -> TreeWalker
end.
to_string(Arg, #dtl_context{ binary_strings = true }) ->
to_binary_string(Arg);
to_string(Arg, #dtl_context{ binary_strings = false }) ->
to_list_string(Arg).
unescape_string_literal(String) ->
unescape_string_literal(remove_quotes(String), [], noslash).
full_path(File, DocRoot) ->
case filename:absname(File) of
File -> File;
_ -> filename:join([DocRoot, File])
end.
print(Fmt, Args, Context) ->
print(?V_INFO, Fmt, Args, Context).
print(Verbosity, Fmt, Args, #treewalker{ context=Context }) ->
print(Verbosity, Fmt, Args, Context);
print(Verbosity, Fmt, Args, #dtl_context{ verbose = Verbose })
when Verbosity =< Verbose ->
io:format(Fmt, Args);
print(_Verbosity, _Fmt, _Args, _Context) ->
ok.
get_current_file(#treewalker{ context=Context }) ->
get_current_file(Context);
get_current_file(#dtl_context{ parse_trail=[File|_] }) -> File;
get_current_file(#dtl_context{ is_compiling_dir=Dir }) when Dir =/= false -> Dir;
get_current_file(#dtl_context{ doc_root=Root }) -> Root.
add_error(Module, Error, #treewalker{ context=Context }=TreeWalker) ->
TreeWalker#treewalker{ context=add_error(Module, Error, Context) };
add_error(Module, Error, #dtl_context{
errors=#error_info{ report=Report, list=Es }=Ei
}=Context) ->
Item = get_error_item(
Report, "",
get_current_file(Context),
Error, Module),
Context#dtl_context{
errors=Ei#error_info{ list=[Item|Es] }
}.
add_errors(Errors, #treewalker{ context=Context }=TreeWalker) ->
TreeWalker#treewalker{ context=add_errors(Errors, Context) };
add_errors(Errors, Context) ->
lists:foldl(
fun (E, C) -> add_error(?MODULE, E, C) end,
Context, Errors).
add_warning(Module, Warning, #treewalker{ context=Context }=TreeWalker) ->
TreeWalker#treewalker{ context=add_warning(Module, Warning, Context) };
add_warning(Module, Warning, #dtl_context{ warnings=warnings_as_errors }=Context) ->
add_error(Module, Warning, Context);
add_warning(Module, Warning, #dtl_context{
warnings=#error_info{ report=Report, list=Ws }=Wi
}=Context) ->
Item = get_error_item(
Report, "Warning: ",
get_current_file(Context),
Warning, Module),
Context#dtl_context{
warnings=Wi#error_info{ list=[Item|Ws] }
}.
add_warnings(Warnings, #treewalker{ context=Context }=TreeWalker) ->
TreeWalker#treewalker{ context=add_warnings(Warnings, Context) };
add_warnings(Warnings, Context) ->
lists:foldl(
fun (W, C) -> add_warning(?MODULE, W, C) end,
Context, Warnings).
call_extension(#treewalker{ context=Context }, Fun, Args) ->
call_extension(Context, Fun, Args);
call_extension(#dtl_context{ extension_module=undefined }, _Fun, _Args) ->
undefined;
call_extension(#dtl_context{ extension_module=Mod }, Fun, Args)
when is_atom(Mod), is_atom(Fun), is_list(Args) ->
M = case code:is_loaded(Mod) of
false ->
case code:load_file(Mod) of
{module, Mod} ->
Mod;
_ ->
undefined
end;
_ -> Mod
end,
if M /= undefined ->
case erlang:function_exported(M, Fun, length(Args)) of
true ->
apply(M, Fun, Args);
false ->
undefined
end;
true ->
undefined
end.
merge_info(Info1, Info2) when is_record(Info1, ast_info), is_record(Info2, ast_info) ->
merge_info1(record_info(size, ast_info), Info1, Info2, #ast_info{}).
resolve_variable(VarName, TreeWalker) ->
resolve_variable(VarName, undefined, TreeWalker).
resolve_variable(VarName, Default, #treewalker{ context=Context }) ->
case resolve_variable1(Context#dtl_context.local_scopes, VarName) of
undefined ->
case proplists:get_value(VarName, Context#dtl_context.const) of
undefined ->
case proplists:get_value(VarName, Context#dtl_context.vars) of
undefined -> {default, Default, []};
Value -> {default_vars, Value, []}
end;
Value -> {constant, Value, []}
end;
{Value, Filters} -> {scope, Value, Filters}
end.
push_scope(Scope, #treewalker{ context=Context }=TreeWalker) ->
TreeWalker#treewalker{ context=push_scope(Scope, Context) };
push_scope(Scope, #dtl_context{ local_scopes=Scopes }=Context) ->
Context#dtl_context{ local_scopes=[Scope|Scopes] }.
restore_scope(Target, #treewalker{ context=Context }=TreeWalker) ->
TreeWalker#treewalker{ context=restore_scope(Target, Context) };
restore_scope(#treewalker{ context=Target }, Context) ->
restore_scope(Target, Context);
restore_scope(#dtl_context{ local_scopes=Scopes }, Context) ->
Context#dtl_context{ local_scopes=Scopes }.
begin_scope(TreeWalker) -> begin_scope(empty_scope(), TreeWalker).
begin_scope({Scope, Values}, TreeWalker) ->
Id = make_ref(),
{Id, push_scope({Id, Scope, Values}, TreeWalker)}.
end_scope(Fun, Id, AstList, TreeWalker) ->
close_scope(Fun, Id, AstList, TreeWalker).
empty_scope() -> {[], []}.
reset_block_dict(BlockDict, #treewalker{ context=Context }=TreeWalker) ->
TreeWalker#treewalker{ context=reset_block_dict(BlockDict, Context) };
reset_block_dict(BlockDict, Context) ->
Context#dtl_context{ block_dict=BlockDict }.
reset_parse_trail(ParseTrail, #treewalker{ context=Context }=TreeWalker) ->
TreeWalker#treewalker{ context=reset_parse_trail(ParseTrail, Context) };
reset_parse_trail(ParseTrail, Context) ->
Context#dtl_context{ parse_trail=ParseTrail }.
load_library(Lib, Context) -> load_library(none, Lib, [], Context).
load_library(Pos, Lib, Context) -> load_library(Pos, Lib, [], Context).
load_library(Pos, Lib, Which, #treewalker{ context=Context }=TreeWalker) ->
TreeWalker#treewalker{ context=load_library(Pos, Lib, Which, Context) };
load_library(Pos, Lib, Which, Context) ->
case lib_module(Lib, Context) of
{ok, Mod} ->
?LOG_DEBUG(
"~s: Load library '~s' from ~s ~p~n",
[get_current_file(Context), Lib, Mod, Which], Context),
Filters = read_library(Mod, filters, Which),
Tags = read_library(Mod, tags, Which),
Found = Filters ++ Tags,
Missing = lists:filter(
fun (W) -> lists:keyfind(W, 1, Found) == false end,
Which),
add_tags(
Tags,
add_filters(
Filters,
warn_missing(
Missing, {Pos, Lib, Mod}, Context)));
Error ->
?WARN({Pos, Error}, Context)
end.
add_filters(Load, #dtl_context{ filters=Filters }=Context) ->
?LOG_TRACE("Load filters: ~p~n", [Load], Context),
Context#dtl_context{ filters=Load ++ Filters }.
add_tags(Load, #dtl_context{ tags=Tags }=Context) ->
?LOG_TRACE("Load tags: ~p~n", [Load], Context),
Context#dtl_context{ tags=Load ++ Tags }.
%% shorten_filename/1 copied from Erlang/OTP lib/compiler/src/compile.erl
shorten_filename(Name) ->
{ok, Cwd} = file:get_cwd(),
shorten_filename(Name, Cwd).
shorten_filename(Name, Cwd) ->
case lists:prefix(Cwd, Name) of
false -> Name;
true ->
case lists:nthtail(length(Cwd), Name) of
"/"++N -> N;
N -> N
end
end.
push_auto_escape(State, #treewalker{ context=Context }=TreeWalker) ->
TreeWalker#treewalker{ context=push_auto_escape(State, Context) };
push_auto_escape(State, #dtl_context{ auto_escape=AutoEscape }=Context) ->
Context#dtl_context{ auto_escape=[State|AutoEscape] }.
pop_auto_escape(#treewalker{ context=Context }=TreeWalker) ->
TreeWalker#treewalker{ context=pop_auto_escape(Context) };
pop_auto_escape(#dtl_context{ auto_escape=[_|AutoEscape] }=Context)
when length(AutoEscape) > 0 ->
Context#dtl_context{ auto_escape=AutoEscape };
pop_auto_escape(Context) -> Context.
token_pos(Token) when is_tuple(Token) ->
token_pos(tuple_to_list(Token));
token_pos([T|Ts]) when is_tuple(T) ->
case T of
{R, C}=P when is_integer(R), is_integer(C) -> P;
_ -> token_pos(tuple_to_list(T) ++ Ts)
end;
token_pos([T|Ts]) when is_list(T) -> token_pos(T ++ Ts);
token_pos([_|Ts]) -> token_pos(Ts);
token_pos([]) -> none.
is_stripped_token_empty({string, _, S}) ->
[] == [C || C <- S, C /= 32, C /= $\r, C /= $\n, C /= $\t];
is_stripped_token_empty({comment, _}) -> true;
is_stripped_token_empty({comment_tag, _, _}) -> true;
is_stripped_token_empty(_) -> false.
format_error({load_library, Name, Mod, Reason}) ->
io_lib:format("Failed to load library '~p' (~p): ~p", [Name, Mod, Reason]);
format_error({load_from, Name, Mod, Tag}) ->
io_lib:format("'~p' not in library '~p' (~p)", [Tag, Name, Mod]);
format_error({unknown_extension, Tag}) ->
io_lib:format("Unhandled extension: ~p", [Tag]);
format_error(Other) ->
io_lib:format("## Sorry, error description for ~p not yet implemented.~n"
"## Please report this so we can fix it.", [Other]).
%%====================================================================
%% Internal functions
%%====================================================================
to_binary_string(Arg) when is_binary(Arg) -> Arg;
to_binary_string(Arg) when is_list(Arg) -> list_to_binary(Arg);
to_binary_string(Arg) when is_integer(Arg) ->
case erlang:function_exported(erlang, integer_to_binary, 1) of
true -> erlang:integer_to_binary(Arg);
false -> list_to_binary(integer_to_list(Arg))
end;
to_binary_string(Arg) when is_atom(Arg) -> atom_to_binary(Arg, latin1).
to_list_string(Arg) when is_list(Arg) -> Arg;
to_list_string(Arg) when is_binary(Arg) -> binary_to_list(Arg);
to_list_string(Arg) when is_integer(Arg) -> integer_to_list(Arg);
to_list_string(Arg) when is_atom(Arg) -> atom_to_list(Arg).
unescape_string_literal([], Acc, noslash) ->
lists:reverse(Acc);
unescape_string_literal([$\\ | Rest], Acc, noslash) ->
unescape_string_literal(Rest, Acc, slash);
unescape_string_literal([C | Rest], Acc, noslash) ->
unescape_string_literal(Rest, [C | Acc], noslash);
unescape_string_literal("n" ++ Rest, Acc, slash) ->
unescape_string_literal(Rest, [$\n | Acc], noslash);
unescape_string_literal("r" ++ Rest, Acc, slash) ->
unescape_string_literal(Rest, [$\r | Acc], noslash);
unescape_string_literal("t" ++ Rest, Acc, slash) ->
unescape_string_literal(Rest, [$\t | Acc], noslash);
unescape_string_literal([C | Rest], Acc, slash) ->
unescape_string_literal(Rest, [C | Acc], noslash).
get_error_item(Report, Prefix, File, Error, DefaultModule) ->
case compose_error_desc(Error, DefaultModule) of
{Pos, Module, ErrorDesc} ->
new_error_item(Report, Prefix, File, Pos, Module, ErrorDesc);
ErrorItem ->
ErrorItem
end.
compose_error_desc({{Line, Col}=Pos, ErrorDesc}, Module)
when is_integer(Line), is_integer(Col), is_atom(Module) ->
{Pos, Module, ErrorDesc};
compose_error_desc({Line, ErrorDesc}, Module)
when is_integer(Line); Line =:= none ->
{Line, Module, ErrorDesc};
compose_error_desc({{Line, Col}, Module, _}=ErrorDesc, _)
when is_integer(Line), is_integer(Col), is_atom(Module) ->
ErrorDesc;
compose_error_desc({Line, Module, _}=ErrorDesc, _)
when is_integer(Line), is_atom(Module) ->
ErrorDesc;
compose_error_desc({none, Module, _}=ErrorDesc, _)
when is_atom(Module) ->
ErrorDesc;
compose_error_desc({_, InfoList}=ErrorDesc, _)
when is_list(InfoList) -> ErrorDesc;
compose_error_desc(ErrorDesc, Module) ->
{none, Module, ErrorDesc}.
new_error_item(Report, Prefix, File, Pos, Module, ErrorDesc) ->
if Report ->
io:format("~s:~s~s~s~n",
[File, pos_info(Pos), Prefix,
Module:format_error(ErrorDesc)]);
true -> nop
end,
{File, [{Pos, Module, ErrorDesc}]}.
pos_info(none) -> " ";
pos_info(Line) when is_integer(Line) ->
io_lib:format("~b: ", [Line]);
pos_info({Line, Col}) when is_integer(Line), is_integer(Col) ->
io_lib:format("~b:~b ", [Line, Col]).
resolve_variable1([], _VarName) -> undefined;
resolve_variable1([Scope|Scopes], VarName) ->
case lists:keyfind(VarName, 1, get_scope(Scope)) of
false ->
resolve_variable1(Scopes, VarName);
{_, Value} -> {Value, []};
{_, Value, Filters} when is_list(Filters) -> {Value, Filters};
{_, Value, Filter} when is_atom(Filter) -> {Value, [{Filter, []}]};
{_, Value, Filter} -> {Value, [Filter]}
end.
get_scope({_Id, Scope, _Values}) -> Scope;
get_scope(Scope) -> Scope.
merge_info1(1, _, _, Info) -> Info;
merge_info1(FieldIdx, Info1, Info2, Info) ->
Value = lists:umerge(
lists:usort(element(FieldIdx, Info1)),
lists:usort(element(FieldIdx, Info2))),
merge_info1(FieldIdx - 1, Info1, Info2, setelement(FieldIdx, Info, Value)).
close_scope(Fun, Id, AstList, TreeWalker) ->
case merge_scopes(Id, TreeWalker) of
{[], TreeWalker1} -> {AstList, TreeWalker1};
{Values, TreeWalker1} ->
{lists:foldl(
fun ({ScopeId, ScopeValues}, AstAcc) ->
{Pre, Target, Post} = split_ast(ScopeId, AstAcc),
Pre ++ Fun(ScopeValues ++ Target) ++ Post
end,
AstList, Values),
TreeWalker1}
end.
merge_scopes(Id, #treewalker{ context=Context }=TreeWalker) ->
{Values, Scopes} = merge_scopes(Id, Context#dtl_context.local_scopes, []),
{lists:reverse(Values),
TreeWalker#treewalker{
context=Context#dtl_context{
local_scopes = Scopes
} }}.
merge_scopes(Id, [{Id, _Scope, []}|Scopes], Acc) -> {Acc, Scopes};
merge_scopes(Id, [{Id, _Scope, Values}|Scopes], Acc) -> {[{Id, Values}|Acc], Scopes};
merge_scopes(Id, [{_ScopeId, _Scope, []}|Scopes], Acc) ->
merge_scopes(Id, Scopes, Acc);
merge_scopes(Id, [{ScopeId, _Scope, Values}|Scopes], Acc) ->
merge_scopes(Id, Scopes, [{ScopeId, Values}|Acc]);
merge_scopes(Id, [_PlainScope|Scopes], Acc) ->
merge_scopes(Id, Scopes, Acc).
split_ast(Id, AstList) ->
split_ast(Id, AstList, []).
split_ast(_Split, [], {Pre, Acc}) ->
{Pre, lists:reverse(Acc), []};
split_ast(_Split, [], Acc) ->
{[], lists:reverse(Acc), []};
split_ast(Split, [Split|Rest], {Pre, Acc}) ->
{Pre, lists:reverse(Acc), Rest};
split_ast(Split, [Split|Rest], Acc) ->
split_ast(end_scope, Rest, {lists:reverse(Acc), []});
split_ast(Split, [Ast|Rest], {Pre, Acc}) ->
split_ast(Split, Rest, {Pre, [Ast|Acc]});
split_ast(Split, [Ast|Rest], Acc) ->
split_ast(Split, Rest, [Ast|Acc]).
lib_module(Name, #dtl_context{ libraries=Libs }) ->
Mod = proplists:get_value(Name, Libs, Name),
case code:ensure_loaded(Mod) of
{module, Mod} ->
case implements_behaviour(erlydtl_library, Mod) of
true ->
case Mod:version() of
?LIB_VERSION -> {ok, Mod};
V -> {load_library, Name, Mod, {version, V}}
end;
false -> {load_library, Name, Mod, behaviour}
end;
{error, Reason} ->
{load_library, Name, Mod, Reason}
end.
implements_behaviour(Behaviour, Mod) ->
Attrs = Mod:module_info(attributes),
Found =
[B || [B] <- proplists:get_all_values(behaviour, Attrs)] ++
[B || [B] <- proplists:get_all_values(behavior, Attrs)],
[] =:= [Behaviour] -- Found.
read_library(Mod, Section, Which) ->
[{Name, lib_function(Mod, Fun)}
|| {Name, Fun} <- read_inventory(Mod, Section),
Which =:= [] orelse lists:member(Name, Which)].
warn_missing([], _, Context) -> Context;
warn_missing([X|Xs], {Pos, Lib, Mod}=Info, Context) ->
warn_missing(Xs, Info, ?WARN({Pos, {load_from, Lib, Mod, X}}, Context)).
lib_function(_, {Mod, Fun}) ->
lib_function(Mod, Fun);
lib_function(Mod, Fun) ->
%% we could check for lib function availability here.. (sanity check)
{Mod, Fun}.
read_inventory(Mod, Section) ->
[case Item of
{_Name, _Fun} -> Item;
Fun -> {Fun, Fun}
end || Item <- Mod:inventory(Section)].
remove_quotes(String) ->
remove_last_quote(remove_first_quote(String)).
remove_first_quote([34 | Rest]) ->
Rest;
remove_first_quote(String) ->
String.
remove_last_quote(String) ->
lists:reverse(remove_first_quote(lists:reverse(String))).
erlydtl-0.15.0/src/erlydtl_contrib_humanize.erl 0000664 0000000 0000000 00000001030 15047124314 0021641 0 ustar 00root root 0000000 0000000 -module(erlydtl_contrib_humanize).
-export([intcomma/1]).
intcomma(Value) when is_integer(Value) ->
intcomma(integer_to_list(Value));
intcomma(Value) ->
ValueBin = iolist_to_binary(Value),
intcomma(ValueBin, size(ValueBin) rem 3, <<>>).
intcomma(<<>>, _, Acc) ->
Acc;
intcomma(<< C, Rest/bits >>, 0, <<>>) ->
intcomma(Rest, 2, << C >>);
intcomma(<< C, Rest/bits >>, 0, Acc) ->
intcomma(Rest, 2, << Acc/binary, $,, C >>);
intcomma(<< C, Rest/bits >>, N, Acc) ->
intcomma(Rest, N - 1, << Acc/binary, C >>).
erlydtl-0.15.0/src/erlydtl_deps.erl 0000664 0000000 0000000 00000004777 15047124314 0017261 0 ustar 00root root 0000000 0000000 %%%-------------------------------------------------------------------
%%% File: erlydtl_deps.erl
%%% @author Roberto Saccon [http://rsaccon.com]
%%% @author Evan Miller
%%% @copyright 2008 Roberto Saccon, Evan Miller
%%% @doc
%%% ErlyDTL helper module
%%% @end
%%%
%%% The MIT License
%%%
%%% Copyright (c) 2007 Roberto Saccon, Evan Miller
%%%
%%% 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.
%%%
%%% @since 2007-12-16 by Roberto Saccon, Evan Miller
%%%-------------------------------------------------------------------
-module(erlydtl_deps).
-author('rsaccon@gmail.com').
-author('emmiller@gmail.com').
%% API
-export([get_base_dir/0, get_base_dir/1]).
%%====================================================================
%% API
%%====================================================================
%% @spec get_base_dir(Module) -> string()
%% @doc Return the application directory for Module. It assumes Module is in
%% a standard OTP layout application in the ebin or src directory.
get_base_dir(Module) ->
{file, Here} = code:is_loaded(Module),
filename:dirname(filename:dirname(Here)).
%% @spec get_base_dir() -> string()
%% @doc Return the application directory for this application. Equivalent to
%% get_base_dir(?MODULE).
get_base_dir() ->
get_base_dir(?MODULE).
%%====================================================================
%% Internal functions
%%====================================================================
erlydtl-0.15.0/src/erlydtl_filters.erl 0000664 0000000 0000000 00000145673 15047124314 0017777 0 ustar 00root root 0000000 0000000 %%%-------------------------------------------------------------------
%%% File: erlydtl_filters.erl
%%% @author Roberto Saccon [http://rsaccon.com]
%%% @author Evan Miller
%%% @copyright 2008 Roberto Saccon, Evan Miller
%%% @doc
%%% Template filters
%%% @end
%%%
%%% The MIT License
%%%
%%% Copyright (c) 2007 Roberto Saccon, Evan Miller
%%%
%%% 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.
%%%
%%% @since 2007-11-11 by Roberto Saccon, Evan Miller
%%%-------------------------------------------------------------------
-module(erlydtl_filters).
-author('rsaccon@gmail.com').
-author('emmiller@gmail.com').
-author('drew dot gulino at google dot com').
-ifdef(TEST).
-undef(TEST).
-endif.
-define(TEST,"").
%-define(NOTEST,1).
-define(NODEBUG,1).
-include_lib("eunit/include/eunit.hrl").
-ifdef(TEST).
-export([cast_to_float/1,cast_to_integer/1,stringformat_io/7,round/2,unjoin/2,addDefaultURI/1]).
-endif.
-import(erlydtl_time_compat, [monotonic_time/0, unique_integer/0]).
-export([add/2,
addslashes/1,
capfirst/1,
center/2,
cut/2,
date/1,
date/2,
date/3,
date/4,
default/2,
default_if_none/2,
dictsort/2,
dictsortreversed/2,
divisibleby/2,
%escape/, - implemented in erlydtl_compiler
escapejs/1,
filesizeformat/1,
first/1,
fix_ampersands/1,
floatformat/1,
floatformat/2,
force_escape/1,
format_integer/1,
format_number/1,
get_digit/2,
iriencode/1,
join/2,
last/1,
length/1,
length_is/2,
linebreaks/1,
linebreaksbr/1,
linenumbers/1,
ljust/2,
lower/1,
make_list/1,
phone2numeric/1,
pluralize/1,
pluralize/2,
pprint/1,
random/1,
random_num/1,
random_range/1,
removetags/2,
rjust/2,
%safe/, - implemented in erlydtl_compiler
%safeseq/, - implemented in erlydtl_compiler
slice/2,
slugify/1,
stringformat/2,
striptags/1,
time/1,
time/2,
timesince/1,
timesince/2,
timeuntil/1,
timeuntil/2,
title/1,
truncatechars/2,
truncatewords/2,
truncatewords_html/2,
unordered_list/1,
upper/1,
urlencode/1,
urlencode/2,
urlize/1,
urlize/2,
urlizetrunc/2,
wordcount/1,
wordwrap/2,
yesno/2]).
-define(NO_ENCODE(C), ((C >= $a andalso C =< $z) orelse
(C >= $A andalso C =< $Z) orelse
(C >= $0 andalso C =< $9) orelse
(C =:= $\. orelse C =:= $-
orelse C =:= $~ orelse C =:= $_))).
-define(NO_IRI_ENCODE(C), (?NO_ENCODE(C) orelse (
C =:= $/ orelse
C =:= $# orelse
C =:= $[ orelse
C =:= $] orelse
C =:= $= orelse
C =:= $: orelse
C =:= $; orelse
C =:= $$ orelse
C =:= $& orelse
C =:= $( orelse
C =:= $) orelse
C =:= $+ orelse
C =:= $, orelse
C =:= $! orelse
C =:= $? orelse
C =:= $* orelse
C =:= $@ orelse
C =:= $' orelse
C =:= $~))).
-define(KILOBYTE, 1024).
-define(MEGABYTE, (1024 * ?KILOBYTE)).
-define(GIGABYTE, (1024 * ?MEGABYTE)).
-define(SECONDS_PER_MINUTE, 60).
-define(SECONDS_PER_HOUR, (60 * ?SECONDS_PER_MINUTE)).
-define(SECONDS_PER_DAY, (24 * ?SECONDS_PER_HOUR)).
-define(SECONDS_PER_WEEK, (7 * ?SECONDS_PER_DAY)).
-define(SECONDS_PER_MONTH, (30 * ?SECONDS_PER_DAY)).
-define(SECONDS_PER_YEAR, (365 * ?SECONDS_PER_DAY)).
%% @doc Adds to values
add(LHS, RHS) when is_number(LHS), is_number(RHS) ->
LHS + RHS;
add(LHS, RHS) when is_binary(LHS) ->
add(unicode:characters_to_list(LHS), RHS);
add(LHS, RHS) when is_binary(RHS) ->
add(LHS, unicode:characters_to_list(RHS));
add(LHS, RHS) when is_list(LHS), is_list(RHS) ->
case {to_numeric(LHS), to_numeric(RHS)} of
{{number, LHSNum}, {number, RHSNum}} ->
LHSNum + RHSNum;
_ ->
LHS ++ RHS
end;
add(LHS, RHS) when is_list(LHS), is_number(RHS) ->
case to_numeric(LHS) of
{number, LHSNum} ->
LHSNum + RHS;
_ ->
LHS ++ to_string(RHS)
end;
add(LHS, RHS) when is_number(LHS), is_list(RHS) ->
case to_numeric(RHS) of
{number, RHSNum} ->
LHS + RHSNum;
_ ->
to_string(LHS) ++ RHS
end.
to_string(Num) when is_integer(Num) ->
integer_to_list(Num);
to_string(Num) when is_float(Num) ->
float_to_list(Num).
to_numeric(List) ->
try
{number, list_to_integer(List)}
catch
error:badarg ->
try
{number, list_to_float(List)}
catch
error:badarg ->
undefined
end
end.
%% @doc Adds slashes before quotes.
addslashes(Input) when is_binary(Input) ->
addslashes(unicode:characters_to_list(Input));
addslashes(Input) when is_list(Input) ->
addslashes(Input, []).
%% @doc Capitalizes the first character of the value.
capfirst([H|T]) when H >= $a andalso H =< $z ->
[H + $A - $a | T];
capfirst(Other) when is_list(Other) ->
Other;
capfirst(<>) when Byte >= $a andalso Byte =< $z ->
[(Byte + $A - $a)|unicode:characters_to_list(Binary)];
capfirst(Other) when is_binary(Other) ->
Other.
%% @doc Centers the value in a field of a given width.
center(Input, Number) when is_binary(Input) ->
unicode:characters_to_binary(center(unicode:characters_to_list(Input), Number));
center(Input, Number) when is_list(Input) ->
string:centre(Input, Number).
%% @doc Removes all values of arg from the given string.
cut(Input, Arg) when is_binary(Arg) ->
cut(Input, unicode:characters_to_list(Arg));
cut(Input, Arg) when is_binary(Input) ->
cut(unicode:characters_to_list(Input), Arg);
cut(Input, [Char]) when is_list(Input) ->
cut(Input, Char, []).
%% @doc Formats a date according to the default format.
date(Input) ->
date(Input, "F j, Y").
%% @doc Formats a date according to the given format.
date(Input, FormatStr)
when is_tuple(Input)
andalso (size(Input) == 2 orelse size(Input) == 3) ->
erlydtl_dateformat:format(Input, FormatStr);
date(Input, _FormatStr) ->
io:format("Unexpected date parameter: ~p~n", [Input]),
"".
%% @doc Formats a date according to the default format
%% localizing it with provided translation function.
date(Input, TransFun, Locale) ->
date(Input, "F j, Y", TransFun, Locale).
date(Input, FormatStr, TransFun, Locale)
when is_tuple(Input)
andalso (size(Input) == 2 orelse size(Input) == 3) ->
erlydtl_dateformat:format(Input, FormatStr, TransFun, Locale);
date(Input, _FormatStr, _TransFun, _Locale) ->
io:format("Unexpected date parameter: ~p~n", [Input]),
"".
%% @doc If value evaluates to `false', use given default. Otherwise, use the value.
default(Input, Default) ->
case erlydtl_runtime:is_false(Input) of
true -> Default;
false -> Input
end.
%% @doc If (and only if) value is `undefined', use given default. Otherwise, use the value.
default_if_none(undefined, Default) ->
Default;
default_if_none(Input, _) ->
Input.
%% @doc Takes a list of dictionaries or proplists and returns that list sorted by the key given in the argument.
dictsort(DictList, Key) when is_binary(Key) ->
dictsort(DictList, [binary_to_atom(B,latin1) ||
B <- binary:split(Key,<<".">>)]);
dictsort(DictList, Key) ->
case lists:all(
fun(Dict) ->
erlydtl_runtime:find_deep_value(Key, Dict) /= undefined
end, DictList) of
true ->
lists:sort(
fun(K1,K2) ->
erlydtl_runtime:find_deep_value(Key,K1) =<
erlydtl_runtime:find_deep_value(Key,K2)
end, DictList);
false -> error
end.
%% @doc Same as dictsort, but the list is reversed.
dictsortreversed(DictList, Key) ->
lists:reverse(dictsort(DictList, Key)).
%% @doc Returns `true' if the value is divisible by the argument.
divisibleby(Input, Divisor) when is_binary(Input) ->
divisibleby(unicode:characters_to_list(Input), Divisor);
divisibleby(Input, Divisor) when is_list(Input) ->
divisibleby(list_to_integer(Input), Divisor);
divisibleby(Input, Divisor) when is_binary(Divisor) ->
divisibleby(Input, unicode:characters_to_list(Divisor));
divisibleby(Input, Divisor) when is_list(Divisor) ->
divisibleby(Input, list_to_integer(Divisor));
divisibleby(Input, Divisor) when is_integer(Input), is_integer(Divisor) ->
Input rem Divisor =:= 0.
%% @doc Escapes characters for use in JavaScript strings.
escapejs(Input) when is_binary(Input) ->
unicode:characters_to_binary(escapejs(unicode:characters_to_list(Input)));
escapejs(Input) when is_list(Input) ->
escapejs(Input, []).
%% @doc Format the value like a human-readable file size.
filesizeformat(Input) when is_binary(Input) ->
filesizeformat(unicode:characters_to_list(Input));
filesizeformat(Input) when is_list(Input) ->
filesizeformat(list_to_integer(Input));
filesizeformat(Bytes) when is_integer(Bytes), Bytes >= ?GIGABYTE->
filesizeformat(Bytes / ?GIGABYTE, "GB");
filesizeformat(Bytes) when is_integer(Bytes), Bytes >= ?MEGABYTE ->
filesizeformat(Bytes / ?MEGABYTE, "MB");
filesizeformat(Bytes) when is_integer(Bytes), Bytes >= ?KILOBYTE ->
filesizeformat(Bytes / ?KILOBYTE, "KB");
filesizeformat(Bytes) when is_integer(Bytes) ->
integer_to_list(Bytes) ++ " bytes".
%% @doc Returns the first item in a list.
first([First|_Rest]) ->
[First];
first(<>) ->
<>.
%% @doc Replaces ampersands with & entities.
fix_ampersands(Input) when is_binary(Input) ->
fix_ampersands(Input, 0);
fix_ampersands(Input) when is_list(Input) ->
fix_ampersands(Input, []).
%% @doc When used without an argument, rounds a floating-point number to one decimal place
%% -- but only if there's a decimal part to be displayed
floatformat(Number) ->
floatformat(Number, []).
floatformat(Number, Place)
when is_number(Number); is_binary(Number); is_list(Number) ->
floatformat_io(cast_to_float(Number), cast_to_integer(Place));
floatformat(_, _) -> "".
floatformat_io(Number, []) ->
floatformat_io(Number, -1);
floatformat_io(Number, 0) ->
lists:flatten(io_lib:format("~B", [erlang:round(Number)]));
floatformat_io(Number, Precision) when Precision > 0 ->
lists:flatten(io_lib:format("~.*f",[Precision, Number]));
floatformat_io(Number, Precision) when Precision < 0 ->
Round = erlang:round(Number),
RoundPrecision = round(Number, -Precision),
if RoundPrecision == Round ->
floatformat_io(Round, 0);
true ->
floatformat_io(RoundPrecision, -Precision)
end.
round(Number, Precision) ->
P = math:pow(10, Precision),
erlang:round(Number * P) / P.
%% @doc Applies HTML escaping to a string.
force_escape(Input) when is_list(Input) ->
escape(Input, []);
force_escape(Input) when is_binary(Input) ->
escape(Input, 0);
force_escape(Input) ->
Input.
format_integer(Input) when is_integer(Input) ->
integer_to_list(Input);
format_integer(Input) ->
Input.
format_number(Input) when is_integer(Input) ->
integer_to_list(Input);
format_number(Input) when is_float(Input) ->
io_lib:format("~p", [Input]);
format_number(Input) when is_function(Input, 0) ->
format_number(Input());
format_number(Input) ->
Input.
%% @doc Given a whole number, returns the requested digit, where 1 is the right-most digit.
get_digit(Input, Digit) when is_binary(Input) ->
get_digit(unicode:characters_to_list(Input), Digit);
get_digit(Input, Digit) when is_integer(Input) ->
get_digit(integer_to_list(Input), Digit);
get_digit(Input, Digit) when is_binary(Digit) ->
get_digit(Input, unicode:characters_to_list(Digit));
get_digit(Input, Digit) when is_list(Digit) ->
get_digit(Input, list_to_integer(Digit));
get_digit(Input, Digit) when Digit > erlang:length(Input) ->
0;
get_digit(Input, Digit) when Digit > 0 ->
lists:nth(Digit, lists:reverse(Input)) - $0;
get_digit(Input, _) ->
Input.
iriencode(Input) ->
iriencode(unicode:characters_to_list(Input), []).
%% @doc Joins a list with a given separator.
join(Input, Separator) when is_list(Input) ->
join_io(Input, Separator).
%% @doc Returns the last item in a list.
last(Input) when is_binary(Input) ->
case size(Input) of
0 -> Input;
N ->
Offset = N - 1,
<<_:Offset/binary, Byte/binary>> = Input,
Byte
end;
last(Input) when is_list(Input) ->
[lists:last(Input)].
%% @doc Returns the length of the value.
length(Input) when is_list(Input) ->
integer_to_list(erlang:length(Input));
length(Input) when is_binary(Input) ->
integer_to_list(size(Input)).
%% @doc Returns True iff the value's length is the argument.
length_is(Input, Number) when is_list(Input), is_integer(Number) ->
length_is(Input, integer_to_list(Number));
length_is(Input, Number) when is_list(Input), is_list(Number) ->
?MODULE:length(Input) =:= Number.
%% @doc Replaces line breaks in plain text with appropriate HTML
linebreaks(Input) when is_binary(Input) ->
linebreaks(unicode:characters_to_list(Input),[]);
linebreaks(Input) ->
linebreaks(Input,[]).
linebreaks([],Acc) ->
"
">>},
{"|upper",
<<"{{ message|upper }}">>, [{message, "That man has a gun."}],
<<"THAT MAN HAS A GUN.">>},
{"|urlencode",
<<"{{ url|urlencode }}">>, [{url, "You #$*@!!"}],
<<"You%20%23%24%2A%40%21%21">>},
{"|urlencode",
<<"{{ url|urlencode }}">>, [{url, "http://www.example.org/foo?a=b&c=d"}],
<<"http%3A//www.example.org/foo%3Fa%3Db%26c%3Dd">>},
{"|urlencode",
<<"{{ url|urlencode:\"\" }}">>, [{url, "http://www.example.org/foo?a=b&c=d"}],
<<"http%3A%2F%2Fwww.example.org%2Ffoo%3Fa%3Db%26c%3Dd">>},
{"|urlencode",
<<"{{ url|urlencode:\":/?=&\" }}">>, [{url, "http://www.example.org/foo?a=b&c=d"}],
<<"http://www.example.org/foo?a=b&c=d">>},
{"|urlize",
<<"{{ var|urlize }}">>, [{var, "Check out www.djangoproject.com"}],
<<"Check out www.djangoproject.com">>},
{"|urlize",
<<"{{ var|urlize }}">>, [{var, "Check out http://www.djangoproject.com"}],
<<"Check out http://www.djangoproject.com">>},
{"|urlize",
<<"{{ var|urlize }}">>, [{var, "Check out \"http://www.djangoproject.com\""}],
<<"Check out \"http://www.djangoproject.com\"">>},
{"|urlizetrunc:15",
<<"{{ var|urlizetrunc:15 }}">>, [{var, "Check out www.djangoproject.com"}],
<<"Check out www.djangopr...">>},
{"|wordcount",
<<"{{ words|wordcount }}">>, [{words, "Why Hello There!"}],
<<"3">>},
{"|wordwrap:2",
<<"{{ words|wordwrap:2 }}">>, [{words, "this is"}],
<<"this \nis">>},
{"|wordwrap:100",
<<"{{ words|wordwrap:100 }}">>, [{words, "testing testing"}],
<<"testing testing">>},
{"|wordwrap:10",
<<"{{ words|wordwrap:10 }}">>, [{words, ""}],
<<"">>},
{"|wordwrap:1",
<<"{{ words|wordwrap:1 }}">>, [{words, "two"}],
<<"two">>},
%% yesno match: \?assert.*\( (.*), erlydtl_filters:(.*)\((.*), "(.*)"\)\)
%% yesno replace: \t\t{"|$2:\"$4\"",\n\t\t\t\t\t <<"{{ var|$2:\"$4\" }}">>, [{var, $3}],\n\t\t\t\t\t<<$1>>}
{"|yesno:\"yeah,no,maybe\"",
<<"{{ var|yesno:\"yeah,no,maybe\" }}">>, [{var, true}],
<<"yeah">>},
{"|yesno:\"yeah,no,maybe\"",
<<"{{ var|yesno:\"yeah,no,maybe\" }}">>, [{var, false}],
<<"no">>},
{"|yesno:\"yeah,no\"",
<<"{{ var|yesno:\"yeah,no\" }}">>, [{var, undefined}],
<<"no">>},
{"|yesno:\"yeah,no,maybe\"",
<<"{{ var|yesno:\"yeah,no,maybe\" }}">>, [{var, undefined}],
<<"maybe">>},
{"string |yesno:\"yeah,no,maybe\"",
<<"{{ var|yesno:\"yeah,no,maybe\" }}">>, [{var, "non-empty string"}],
<<"yeah">>},
{"binary |yesno:\"yeah,no,maybe\"",
<<"{{ var|yesno:\"yeah,no,maybe\" }}">>, [{var, <<"non-empty binary">>}],
<<"yeah">>},
{"empty string |yesno:\"yeah,no,maybe\"",
<<"{{ var|yesno:\"yeah,no,maybe\" }}">>, [{var, ""}],
<<"no">>},
{"empty binary |yesno:\"yeah,no\"",
<<"{{ var|yesno:\",no\" }}">>, [{var, <<"">>}],
<<"no">>},
{"term |yesno:\"yeah,,maybe\"",
<<"{{ var|yesno:\"yeah,no,maybe\" }}">>, [{var, {my, [term, "test"]}}],
<<"yeah">>},
{"|yesno:\"yeah,\"",
<<"{{ var|yesno:\"yeah,\" }}">>, [{var, false}],
<<"">>},
{"|yesno:\"yeah,,maybe\"",
<<"{{ var|yesno:\"yeah,,maybe\" }}">>, [{var, false}],
<<"">>},
#test{
title = "|yesno:\"missing_false_choice\"",
source = <<"{{ var|yesno:\"missing_false_choice\" }}">>,
render_vars = [{var, true}],
output = {error, {yesno, choices}}
},
{"escape only once (#150) - no auto escape",
%% note that auto_escape is off by default in the test suite
%% due to how the tests have been written (and it's too much
%% work for me to rewrite them)
<<"{{ foo }}{{ foo|add:'bar' }}">>,
[{foo, "foo&"}],
<<"foo&foo&bar">>},
{"escape only once (#150) - auto escape block",
<<"{% autoescape on %}{{ foo }}{{ foo|add:'bar' }}{% endautoescape %}">>,
[{foo, "foo&"}],
<<"foo&foo&bar">>},
{"escape only once (#150) - auto escape",
<<"{{ foo }}{{ foo|add:'bar' }}">>,
[{foo, "foo&"}], [], [auto_escape],
<<"foo&foo&bar">>},
{"escape only once (#150) - auto escape, safe",
<<"{{ foo|safe }}{{ foo|add:'bar'|safe }}&{{ foo|safe|add:'bar' }}">>,
[{foo, "foo&"}], [], [auto_escape],
<<"foo&foo&bar&foo&bar">>},
{"escape only once (#150) - escape filter",
<<"{{ foo|escape }}{{ foo|add:'bar'|escape }}&{{ foo|escape|add:'bar' }}">>,
[{foo, "foo&"}],
<<"foo&foo&bar&foo&bar">>},
{"escape only once (#150) - auto escape + escape filter",
<<"{{ foo|escape }}{{ foo|add:'bar'|escape }}&{{ foo|escape|add:'bar' }}">>,
[{foo, "foo&"}], [], [auto_escape],
<<"foo&foo&bar&foo&bar">>}
]},
{"filters_if",
[{"Filter if 1.1",
<<"{% if var1|length_is:0 %}Y{% else %}N{% endif %}">>,
[{var1, []}],
<<"Y">>},
{"Filter if 1.2",
<<"{% if var1|length_is:1 %}Y{% else %}N{% endif %}">>,
[{var1, []}],
<<"N">>},
{"Filter if 1.3",
<<"{% if var1|length_is:7 %}Y{% else %}N{% endif %}">>,
[{var1, []}],
<<"N">>},
{"Filter if 2.1",
<<"{% if var1|length_is:0 %}Y{% else %}N{% endif %}">>,
[{var1, ["foo"]}],
<<"N">>},
{"Filter if 2.2",
<<"{% if var1|length_is:1 %}Y{% else %}N{% endif %}">>,
[{var1, ["foo"]}],
<<"Y">>},
{"Filter if 2.3",
<<"{% if var1|length_is:7 %}Y{% else %}N{% endif %}">>,
[{var1, ["foo"]}],
<<"N">>},
{"Filter if 3.1",
<<"{% ifequal var1|length 0 %}Y{% else %}N{% endifequal %}">>,
[{var1, []}],
<<"Y">>},
{"Filter if 3.2",
<<"{% ifequal var1|length 1 %}Y{% else %}N{% endifequal %}">>,
[{var1, []}],
<<"N">>},
{"Filter if 4.1",
<<"{% ifequal var1|length 3 %}Y{% else %}N{% endifequal %}">>,
[{var1, ["foo", "bar", "baz"]}],
<<"Y">>},
{"Filter if 4.2",
<<"{% ifequal var1|length 0 %}Y{% else %}N{% endifequal %}">>,
[{var1, ["foo", "bar", "baz"]}],
<<"N">>},
{"Filter if 4.3",
<<"{% ifequal var1|length 1 %}Y{% else %}N{% endifequal %}">>,
[{var1, ["foo", "bar", "baz"]}],
<<"N">>}
]},
{"firstof",
[{"Firstof first",
<<"{% firstof foo bar baz %}">>,
[{foo, "1"},{bar, "2"}],
<<"1">>},
{"Firstof second",
<<"{% firstof foo bar baz %}">>,
[{bar, "2"}],
<<"2">>},
{"Firstof none",
<<"{% firstof foo bar baz %}">>,
[],
<<"">>},
{"Firstof complex",
<<"{% firstof foo.bar.baz bar %}">>,
[{foo, [{bar, [{baz, "quux"}]}]}],
<<"quux">>},
{"Firstof undefined complex",
<<"{% firstof foo.bar.baz bar %}">>,
[{bar, "bar"}],
<<"bar">>},
{"Firstof literal",
<<"{% firstof foo bar \"baz\" %}">>,
[],
<<"baz">>}
]},
{"regroup .. endregroup",
[{"Ordered",
<<"{% regroup people by gender as gender_list %}{% for gender in gender_list %}{{ gender.grouper }}\n{% for item in gender.list %}{{ item.first_name }}\n{% endfor %}{% endfor %}{% endregroup %}">>,
[{people, [[{first_name, "George"}, {gender, "Male"}], [{first_name, "Bill"}, {gender, "Male"}],
[{first_name, "Margaret"}, {gender, "Female"}], [{first_name, "Condi"}, {gender, "Female"}]]}],
<<"Male\nGeorge\nBill\nFemale\nMargaret\nCondi\n">>},
{"Unordered",
<<"{% regroup people by gender as gender_list %}{% for gender in gender_list %}{{ gender.grouper }}\n{% for item in gender.list %}{{ item.first_name }}\n{% endfor %}{% endfor %}{% endregroup %}">>,
[{people, [[{first_name, "George"}, {gender, "Male"}],
[{first_name, "Margaret"}, {gender, "Female"}],
[{first_name, "Condi"}, {gender, "Female"}],
[{first_name, "Bill"}, {gender, "Male"}]
]}],
<<"Male\nGeorge\nFemale\nMargaret\nCondi\nMale\nBill\n">>},
{"NestedOrdered",
<<"{% regroup people by name.last as lastname_list %}{% for lastname in lastname_list %}{{ lastname.grouper }}\n{% for item in lastname.list %}{{ item.name.first }}\n{% endfor %}{% endfor %}{% endregroup %}">>,
[{people, [[{name, [{first,"George"},{last,"Costanza"}]}],
[{name, [{first,"Margaret"},{last,"Costanza"}]}],
[{name, [{first,"Bill"},{last,"Buffalo"}]}],
[{name, [{first,"Condi"},{last,"Buffalo"}]}]]}],
<<"Costanza\nGeorge\nMargaret\nBuffalo\nBill\nCondi\n">>},
{"NestedUnordered",
<<"{% regroup people by name.last as lastname_list %}{% for lastname in lastname_list %}{{ lastname.grouper }}\n{% for item in lastname.list %}{{ item.name.first }}\n{% endfor %}{% endfor %}{% endregroup %}">>,
[{people, [[{name, [{first,"George"},{last,"Costanza"}]}],
[{name, [{first,"Bill"},{last,"Buffalo"}]}],
[{name, [{first,"Margaret"},{last,"Costanza"}]}],
[{name, [{first,"Condi"},{last,"Buffalo"}]}]]}],
<<"Costanza\nGeorge\nBuffalo\nBill\nCostanza\nMargaret\nBuffalo\nCondi\n">>},
{"Filter",
<<"{% regroup people|dictsort:\"name.last\" by name.last as lastname_list %}{% for lastname in lastname_list %}{{ lastname.grouper }}\n{% for item in lastname.list %}{{ item.name.first }}\n{% endfor %}{% endfor %}{% endregroup %}">>,
[{people, [[{name, [{first,"George"},{last,"Costanza"}]}],
[{name, [{first,"Bill"},{last,"Buffalo"}]}],
[{name, [{first,"Margaret"},{last,"Costanza"}]}],
[{name, [{first,"Condi"},{last,"Buffalo"}]}]]}],
<<"Buffalo\nBill\nCondi\nCostanza\nGeorge\nMargaret\n">>}
]},
{"regroup",
[{"Ordered",
<<"{% regroup people by gender as gender_list %}{% for gender in gender_list %}{{ gender.grouper }}\n{% for item in gender.list %}{{ item.first_name }}\n{% endfor %}{% endfor %}">>,
[{people, [[{first_name, "George"}, {gender, "Male"}], [{first_name, "Bill"}, {gender, "Male"}],
[{first_name, "Margaret"}, {gender, "Female"}], [{first_name, "Condi"}, {gender, "Female"}]]}],
<<"Male\nGeorge\nBill\nFemale\nMargaret\nCondi\n">>},
{"Unordered",
<<"{% regroup people by gender as gender_list %}{% for gender in gender_list %}{{ gender.grouper }}\n{% for item in gender.list %}{{ item.first_name }}\n{% endfor %}{% endfor %}">>,
[{people, [[{first_name, "George"}, {gender, "Male"}],
[{first_name, "Margaret"}, {gender, "Female"}],
[{first_name, "Condi"}, {gender, "Female"}],
[{first_name, "Bill"}, {gender, "Male"}]
]}],
<<"Male\nGeorge\nFemale\nMargaret\nCondi\nMale\nBill\n">>},
{"NestedOrdered",
<<"{% regroup people by name.last as lastname_list %}{% for lastname in lastname_list %}{{ lastname.grouper }}\n{% for item in lastname.list %}{{ item.name.first }}\n{% endfor %}{% endfor %}">>,
[{people, [[{name, [{first,"George"},{last,"Costanza"}]}],
[{name, [{first,"Margaret"},{last,"Costanza"}]}],
[{name, [{first,"Bill"},{last,"Buffalo"}]}],
[{name, [{first,"Condi"},{last,"Buffalo"}]}]]}],
<<"Costanza\nGeorge\nMargaret\nBuffalo\nBill\nCondi\n">>},
{"NestedUnordered",
<<"{% regroup people by name.last as lastname_list %}{% for lastname in lastname_list %}{{ lastname.grouper }}\n{% for item in lastname.list %}{{ item.name.first }}\n{% endfor %}{% endfor %}">>,
[{people, [[{name, [{first,"George"},{last,"Costanza"}]}],
[{name, [{first,"Bill"},{last,"Buffalo"}]}],
[{name, [{first,"Margaret"},{last,"Costanza"}]}],
[{name, [{first,"Condi"},{last,"Buffalo"}]}]]}],
<<"Costanza\nGeorge\nBuffalo\nBill\nCostanza\nMargaret\nBuffalo\nCondi\n">>},
{"Filter",
<<"{% regroup people|dictsort:\"name.last\" by name.last as lastname_list %}{% for lastname in lastname_list %}{{ lastname.grouper }}\n{% for item in lastname.list %}{{ item.name.first }}\n{% endfor %}{% endfor %}">>,
[{people, [[{name, [{first,"George"},{last,"Costanza"}]}],
[{name, [{first,"Bill"},{last,"Buffalo"}]}],
[{name, [{first,"Margaret"},{last,"Costanza"}]}],
[{name, [{first,"Condi"},{last,"Buffalo"}]}]]}],
<<"Buffalo\nBill\nCondi\nCostanza\nGeorge\nMargaret\n">>},
{"With surrounding context",
<<"People: {% regroup people by gender as gender_list %}{% for gender in gender_list %}{{ gender.grouper }}\n{% for item in gender.list %}{{ item.first_name }}\n{% endfor %}{% endfor %}Done.">>,
[{people, [[{first_name, "George"}, {gender, "Male"}], [{first_name, "Bill"}, {gender, "Male"}],
[{first_name, "Margaret"}, {gender, "Female"}], [{first_name, "Condi"}, {gender, "Female"}]]}],
<<"People: Male\nGeorge\nBill\nFemale\nMargaret\nCondi\nDone.">>},
#test{
title = "regroup record",
source = <<"{% regroup people by gender as gender_list %}{% for gender in gender_list %}{{ gender.grouper }}:\n{% for person in gender.list %} - {{ person.first_name }}\n{% endfor %}{% endfor %}">>,
compile_opts = [{record_info, [{person, record_info(fields, person)}]} | (#test{})#test.compile_opts],
render_vars = [{people, [#person{ first_name = "George", gender = "Male" },
#person{ first_name = "Bill", gender = "Male" },
#person{ first_name = "Margaret", gender = "Female" },
#person{ first_name = "Condi", gender = "Female" }
]}
],
output = <<"Male:\n - George\n - Bill\nFemale:\n - Margaret\n - Condi\n">>
}
]},
{"spaceless",
[{"Beginning", <<"{% spaceless %} foo{% endspaceless %}">>, [], <<"foo">>},
{"Middle", <<"{% spaceless %}foobar{% endspaceless %}">>, [], <<"foobar">>},
{"End", <<"{% spaceless %}foo {% endspaceless %}">>, [], <<"foo">>},
{"NewLine", <<"{% spaceless %}\n
\n foo \n
\n {% endspaceless %}">>, [], <<"
foo
">>}
]},
{"templatetag",
[{"openblock", <<"{% templatetag openblock %}">>, [], <<"{%">>},
{"closeblock", <<"{% templatetag closeblock %}">>, [], <<"%}">>},
{"openvariable", <<"{% templatetag openvariable %}">>, [], <<"{{">>},
{"closevariable", <<"{% templatetag closevariable %}">>, [], <<"}}">>},
{"openbrace", <<"{% templatetag openbrace %}">>, [], <<"{">>},
{"closebrace", <<"{% templatetag closebrace %}">>, [], <<"}">>},
{"opencomment", <<"{% templatetag opencomment %}">>, [], <<"{#">>},
{"closecomment", <<"{% templatetag closecomment %}">>, [], <<"#}">>}
]},
{"trans",
[{"trans functional default locale",
<<"Hello {% trans \"Hi\" %}">>, [], <<"Hello Hi">>
},
{"trans functional reverse locale",
<<"Hello {% trans \"Hi\" %}">>, [], [{locale, "reverse"}],
[{locales, ["reverse"]}, {translation_fun, fun("Hi"=Key, "reverse") -> list_to_binary(lists:reverse(Key)) end}],
<<"Hello iH">>
},
{"trans literal at run-time",
<<"Hello {% trans \"Hi\" %}">>, [], [{translation_fun, fun("Hi") -> "Konichiwa" end}], [],
<<"Hello Konichiwa">>},
{"trans variable at run-time",
<<"Hello {% trans var1 %}">>, [{var1, <<"Hi">>}], [{translation_fun, fun(<<"Hi">>) -> <<"Konichiwa">> end}], [],
<<"Hello Konichiwa">>},
{"trans literal at run-time: No-op",
<<"Hello {% trans \"Hi\" noop %}">>, [], [{translation_fun, fun("Hi") -> <<"Konichiwa">> end}], [],
<<"Hello Hi">>},
{"trans variable at run-time: No-op",
<<"Hello {% trans var1 noop %}">>, [{var1, <<"Hi">>}], [{translation_fun, fun(<<"Hi">>) -> <<"Konichiwa">> end}], [],
<<"Hello Hi">>},
{"trans as",
<<"{% trans 'Hans' as name %}Hello {{ name }}">>, [],
<<"Hello Hans">>},
{"trans value",
<<"{{ _('foo') }}">>, [], [], [{locale, default}, {translation_fun, fun ("foo") -> "bar" end}],
<<"bar">>},
{"filtered value",
<<"{{ _('foo')|reverse }}">>, [], [],
[{locale, default},
{translation_fun, fun ("foo") -> "bar" end},
{default_libraries, [test1]},
{libraries, [{test1, erlydtl_lib_test1}]}],
<<"rab">>}
]},
{"blocktrans",
[{"blocktrans default locale",
<<"{% blocktrans %}Hello{% endblocktrans %}">>, [], <<"Hello">>},
{"blocktrans choose locale",
<<"{% blocktrans %}Hello, {{ name }}{% endblocktrans %}">>, [{name, "Mr. President"}], [{locale, "de"}],
[{locales, ["de"]}, {translation_fun, fun("Hello, {{ name }}", "de") -> <<"Guten tag, {{ name }}">> end}], <<"Guten tag, Mr. President">>},
{"blocktrans with args",
<<"{% blocktrans with var1=foo %}{{ var1 }}{% endblocktrans %}">>, [{foo, "Hello"}], <<"Hello">>},
#test{
title = "blocktrans blocks in content not allowed",
source = <<"{% blocktrans %}Hello{%if name%}, {{ name }}{%endif%}!{% endblocktrans %}">>,
errors = [error_info([{{1, 24}, erlydtl_parser, ["syntax error before: ",["\"if\""]]}])]
},
#test{
title = "blocktrans nested variables not allowed",
source = <<"{% blocktrans %}Hello, {{ user.name }}!{% endblocktrans %}">>,
errors = [error_info([{{1,31}, erlydtl_parser, ["syntax error before: ","'.'"]}])]
},
{"blocktrans runtime",
<<"{%blocktrans with v1=foo%}Hello, {{ name }}! See {{v1}}.{%endblocktrans%}">>,
[{name, "Mr. President"}, {foo, <<"rubber-duck">>}],
[{translation_fun, fun("Hello, {{ name }}! See {{ v1 }}.") -> <<"Guten tag, {{name}}! Sehen {{ v1 }}.">> end}],
[], <<"Guten tag, Mr. President! Sehen rubber-duck.">>},
{"trimmed",
<<"{% blocktrans trimmed %}\n foo \n bar here .\n \n \n baz{% endblocktrans %}">>,
[], [{translation_fun, fun ("foo bar here . baz") -> "ok" end}],
<<"ok">>}
]},
{"extended translation features (#131)",
[{"trans default locale",
<<"test {% trans 'message' %}">>,
[], [{translation_fun, fun ("message", default) -> "ok" end}],
<<"test ok">>},
{"trans foo locale",
<<"test {% trans 'message' %}">>,
[], [{locale, "foo"}, {translation_fun, fun ("message", "foo") -> "ok" end}],
<<"test ok">>},
{"trans context (run-time)",
<<"test {% trans 'message' context 'foo' %}">>,
[], [{translation_fun, fun ("message", {default, "foo"}) -> "ok" end}],
<<"test ok">>},
{"trans context (compile-time)",
<<"test {% trans 'message' context 'foo' %}">>,
[], [{locale, "baz"}],
[{locales, ["bar", "baz"]},
{translation_fun, fun ("message", {L, "foo"}) ->
case L of
"bar" -> "rab";
"baz" -> "ok"
end
end}],
<<"test ok">>},
{"trans context noop",
<<"{% trans 'message' noop context 'foo' %}">>, [], [],
<<"message">>},
{"blocktrans context (run-time)",
<<"{% blocktrans context 'bar' %}translate this{% endblocktrans %}">>,
[], [{locale, "foo"}, {translation_fun,
fun ("translate this", {"foo", "bar"}) ->
"got it"
end}],
<<"got it">>},
{"blocktrans context (compile-time)",
<<"{% blocktrans context 'bar' %}translate this{% endblocktrans %}">>,
[], [{locale, "foo"}],
[{locale, "foo"}, {translation_fun,
fun ("translate this", {"foo", "bar"}) ->
"got it"
end}],
<<"got it">>},
{"blocktrans plural",
<<"{% blocktrans count foo=bar %}",
"There is just one foo..",
"{% plural %}",
"There are many foo's..",
"{% endblocktrans %}">>,
[{bar, 2}], [{locale, "baz"},
{translation_fun,
fun ({"There is just one foo..", {"There are many foo's..", 2}}, "baz") ->
"ok"
end}],
<<"ok">>},
{"blocktrans a lot of stuff",
<<"{% blocktrans with foo=a.b count c=a|length context 'quux' %}"
"foo={{ foo }};bar={{ bar }};c={{ c }}:"
"{% plural %}"
"FOO:{{ foo }},BAR:{{ bar }},C:{{ c }}."
"{% endblocktrans %}">>,
[{a, [{b, "B"}]}, {bar, "BAR"}],
[{locale, "rub"},
{translation_fun, fun ({Single, {Plural, "1"=_Count}}, {Locale, Context}) ->
[Single, Plural, Locale, Context]
end}],
<<"foo=B;bar=BAR;c=1:"
"FOO:B,BAR:BAR,C:1."
"rub" "quux">>},
{"new translation options",
<<"{% trans foo %}{% blocktrans %}abc{% endblocktrans %}">>,
[{foo, "1234"}], [{locale, "test"}, {translation_fun, fun (Msg) -> lists:reverse(Msg) end}],
[{locale, "foo"}, {locale, "test"}, {locales, ["bar", "baz"]},
{translation_fun, fun (Msg, _) -> [Msg, lists:reverse(Msg)] end}],
<<"4321" "abccba">>}
%% This does work, but always prints a warning to std err.. :/
%% Warning: template translation: variable not closed: "bar {{ 123"
%% {"variable error",
%% <<"{% blocktrans %}foo{{ bar }}{% endblocktrans %}">>,
%% [], [{translation_fun, fun (_) -> "bar {{ 123" end}],
%% <<"foo">>}
]},
{"i18n",
[{"setup translation context, using fun, at render time",
<<"{% trans 'foo' %}">>, [],
[{translation_fun, fun () -> fun (Msg) -> string:to_upper(Msg) end end}],
<<"FOO">>},
{"setup translation context, using fun, at compile time",
<<"{% trans 'foo' %}">>, [], [],
[{locale, default}, {translation_fun, fun () -> fun lists:reverse/1 end}],
<<"oof">>}
]},
{"language",
[{"override locale",
<<"{% trans 'foo' %}{% language 'other' %}{% trans 'foo' %}{% endlanguage %}">>,
[], [{locale, <<"default">>}, {translation_fun, fun ("foo", <<"default">>) -> "1"; ("foo", <<"other">>) -> "2"; (A, B) -> [A, B] end}],
<<"12">>}
]},
{"verbatim",
[{"Plain verbatim",
<<"{% verbatim %}{{ oh no{% foobar %}{% endverbatim %}">>, [],
<<"{{ oh no{% foobar %}">>},
{"Named verbatim",
<<"{% verbatim foobar %}{% verbatim %}{% endverbatim foobar2 %}{% endverbatim foobar %}">>, [],
<<"{% verbatim %}{% endverbatim foobar2 %}">>}
]},
{"widthratio",
[{"Literals", <<"{% widthratio 5 10 100 %}">>, [], <<"50">>},
{"Rounds up", <<"{% widthratio a b 100 %}">>, [{a, 175}, {b, 200}], <<"88">>}
]},
{"with",
[{"Cache literal",
<<"{% with a=1 %}{{ a }}{% endwith %}">>, [], <<"1">>},
{"Cache variable",
<<"{% with a=b %}{{ a }}{% endwith %}">>, [{b, "foo"}], <<"foo">>},
{"Cache variable with attribute",
<<"I enjoy {% with a = var1 %}{{ a.game }}{% endwith %}">>, [{var1, [{game, "Othello"}]}], <<"I enjoy Othello">>},
{"Cache variable attribute",
<<"I enjoy {% with a = var1.game %}{{ a }}{% endwith %}">>, [{var1, [{game, "Othello"}]}], <<"I enjoy Othello">>},
{"Cache multiple",
<<"{% with alpha=1 beta=b %}{{ alpha }}/{{ beta }}{% endwith %}">>, [{b, 2}], <<"1/2">>}
]},
{"unicode",
[{"(tm) somewhere",
<<"â„¢">>, [], <<"â„¢">>}
]},
{"contrib_humanize",
[{"intcomma",
<<"{{ a|intcomma }} {{ b|intcomma }} {{ c|intcomma }} {{ d|intcomma }}">>,
[{a, 999}, {b, 123456789}, {c, 12345}, {d, 1234567890}], [],
[{custom_filters_modules, [erlydtl_contrib_humanize]}],
<<"999 123,456,789 12,345 1,234,567,890">>}
]},
%% custom syntax stuff
{"extension_module",
[ %% the erlydtl_test_extension module replaces a foo identifier with bar when hitting a # following foo.
{"replace parsed token", <<"{{ foo # }}">>, [{bar, "ok"}], [],
[{extension_module, erlydtl_test_extension}], <<"ok">>},
#test{
title = "proper error message",
source = <<"{{ bar # }}">>,
render_vars = [{bar, "ok"}],
compile_opts = [{extension_module, erlydtl_test_extension},
report, return, force_recompile, {out_dir, false}],
errors = [error_info([{{1,8},erlydtl_scanner,{illegal_char, $#}}])]
},
%% accept identifiers as expressions (this is a dummy functionality to test the parser extensibility)
{"identifiers as expressions", <<"{{ foo.bar or baz }}">>, [{baz, "ok"}], [],
[{extension_module, erlydtl_test_extension}], <<"ok">>}
]},
{"records",
[{"field access",
<<"{{ r.baz }}">>, [{r, #testrec{ foo="Foo", bar="Bar", baz="Baz" }}], [],
[{record_info, [{testrec, record_info(fields, testrec)}]}],
<<"Baz">>}
]},
{"error reporting",
[#test{
title = "no out dir warning",
source = <<"foo bar">>,
compile_opts = [report, return, force_recompile],
output = <<"foo bar">>,
warnings = [error_info([no_out_dir])]
},
#test{
title = "warnings as errors",
source = <<"foo bar">>,
compile_opts = [report, return, warnings_as_errors, force_recompile],
errors = [error_info([no_out_dir])]
},
#test{
title = "illegal character",
source = <<"{{{">>,
errors = [error_info([{{1,3},erlydtl_scanner,{illegal_char, ${}}])]
},
#test{
title = "unexpected end of file - in code",
source = <<"{{">>,
errors = [error_info([{{1,3},erlydtl_scanner,{eof, in_code}}])]
},
#test{
title = "unexpected end of file - in comment",
source = <<"{#">>,
errors = [error_info([{{1,3},erlydtl_scanner,{eof, in_comment}}])]
},
{"unknown library",
<<"{% load foo %}">>, [], [], [],
<<>>,
[error_info(
[{{1,9},erlydtl_compiler_utils,{load_library,foo,foo,nofile}}
])]
},
{"not a library",
<<"{% load foo %}">>, [], [],
[{libraries, [{foo, ?MODULE}]}],
<<>>,
[error_info(
[{{1,9},erlydtl_compiler_utils,{load_library,foo,?MODULE,behaviour}}
])]
},
{"library version",
<<"{% load foo %}">>, [], [],
[{libraries, [{foo, erlydtl_lib_testversion}]}],
<<>>,
[error_info(
[{{1,9},erlydtl_compiler_utils,{load_library,foo,erlydtl_lib_testversion,{version,invalid}}}
])]
},
{"not in library",
<<"{% load foo bar from test1 %}\n{{ \"w00t\"|reverse }}">>, [], [],
[{libraries, [{test1, erlydtl_lib_test1}]}],
<<"\n">>,
[error_info(
[{{2,11},erlydtl_beam_compiler,{unknown_filter,reverse,1}},
{{1,22},erlydtl_compiler_utils,{load_from,test1,erlydtl_lib_test1,foo}},
{{1,22},erlydtl_compiler_utils,{load_from,test1,erlydtl_lib_test1,bar}}
])]
},
{"pre load unknown library",
<<"{{ '123'|reverse }}">>, [], [],
[{default_libraries, [test1]}],
<<"">>,
[error_info(
[{{1,10},erlydtl_beam_compiler,{unknown_filter,reverse,1}},
{none,erlydtl_compiler_utils,{load_library,test1,test1,nofile}}
])]
},
{"pre load unknown legacy library",
<<"{% foo %}">>, [], [],
[{custom_tags_modules, [foo]}],
<<"">>,
[error_info(
[{none,erlydtl_beam_compiler,{unknown_tag, foo}},
{none,erlydtl_compiler,{load_library,'(custom-legacy)',foo,nofile}}
])]
},
{"unknown filter",
<<"{{ '123'|foo }}">>, [], [], [],
<<"">>,
[error_info([{{1,10},erlydtl_beam_compiler,{unknown_filter,foo,1}}])]
},
{"unknown tag",
<<"a{% b %}c">>, [], [], [],
<<"ac">>,
[error_info([{none,erlydtl_beam_compiler,{unknown_tag, b}}])]
},
{"ssi file not found",
<<"{% ssi 'foo' %}">>, [],
{error, {read_file, <<"./foo">>, enoent}}
},
{"deprecated compile options",
<<"">>, [], [],
[{blocktrans_locales, []}, {blocktrans_fun, fun (_) -> [] end}],
<<"">>,
[error_info([{deprecated_option, O, N}
|| {O, N} <- [{blocktrans_locales, locales},
{blocktrans_fun, translation_fun}]],
erlydtl_compiler)]
}
]},
{"load",
[{"filter",
<<"{% load test1 %}{{ \"1234\"|reverse }}">>, [], [],
[{libraries, [{test1, erlydtl_lib_test1}]}],
<<"4321">>
},
{"named",
<<"{% load reverse from test1 %}{{ \"abcd\"|reverse }}">>, [], [],
[{libraries, [{test1, erlydtl_lib_test1}]}],
<<"dcba">>
},
{"pre loaded",
<<"{{ QWER|reverse }}">>, [{'QWER', "Qwerty"}], [],
[{default_libraries, [test1]},
{libraries, [{test1, erlydtl_lib_test1}]}],
<<"ytrewQ">>
},
{"lib with multiple behaviours",
<<"{{ QWER|reverse }}">>, [{'QWER', "Qwerty"}], [],
[{default_libraries, [test2]},
{libraries, [{test2, erlydtl_lib_test2}]}],
<<"ytrewQ">>
},
{"lib with multiple behaviors (alternative spelling)",
<<"{{ QWER|reverse }}">>, [{'QWER', "Qwerty"}], [],
[{default_libraries, [test2]},
{libraries, [{test2, erlydtl_lib_test2a}]}],
<<"ytrewQ">>
}
]},
{"compile time default vars/constants",
begin
Tpl = <<"Test {{ var1 }}:{{ var2 }}.">>,
Txt = <<"Test 123:abc.">>,
Fun = fun (F) ->
fun (#test{ module=M }) ->
M:F()
end
end,
[{"default vars",
Tpl, [], [],
[{default_vars, [{var1, 123}, {var2, abc}]}], Txt},
{"default vars (using fun)",
Tpl, [], [],
[{default_vars, [{var1, 123}, {var2, fun () -> abc end}]}], Txt},
{"override default vars",
Tpl, [{var2, abc}], [],
[{default_vars, [{var1, 123}, {var2, 456}]}], Txt},
{"constants",
Tpl, [], [],
[{constants, [{var1, 123}, {var2, abc}]}], Txt},
{"constants (using fun)",
Tpl, [], [],
[{constants, [{var1, 123}, {var2, fun () -> abc end}]}], Txt},
{"constants non-overridable",
Tpl, [{var1, ohno}, {var2, noway}], [],
[{constants, [{var1, 123}, {var2, "abc"}]}], Txt}
|[#test{ title = T,
source = Tpl,
compile_vars = undefined,
compile_opts = CO ++ (#test{})#test.compile_opts,
renderer = Fun(F),
output = O
}
|| {T, F, O, CO} <-
[{"variables/0",
variables, [var1, var2], []},
{"variables/0 w. defaults",
variables, [var1, var2], [{default_vars, [{var1, aaa}]}]},
{"variables/0 w. constants",
variables, [var2], [{constants, [{var1, bbb}]}]},
{"default_variables/0",
default_variables, [], []},
{"default_variables/0 w. defaults",
default_variables, [var1], [{default_vars, [{var1, aaa}]}]},
{"default_variables/0 w. constants",
default_variables, [], [{constants, [{var1, bbb}]}]},
{"constants/0",
constants, [], []},
{"constants/0 w. defaults",
constants, [], [{default_vars, [{var1, aaa}]}]},
{"constants/0 w. constants",
constants, [var1], [{constants, [{var1, bbb}]}]}
]
]]
end},
{"functional",
[functional_test(F)
%% order is important for a few of these tests, unfortunately.
|| F <- ["autoescape", "comment", "extends", "filters", "for", "for_list", "for_tuple",
"for_list_preset", "for_preset", "for_records", "for_records_preset", "include",
"if", "if_preset", "ifequal", "ifequal_preset", "ifnotequal", "ifnotequal_preset",
"now", "var", "var_preset", "cycle", "custom_tag", "custom_tag1", "custom_tag2",
"custom_tag3", "custom_tag4", "custom_tag_var", "custom_tag_lib_var", "custom_call", "include_template", "include_path",
"ssi", "extends_path", "extends_path2", "trans", "extends_for", "extends2",
"extends3", "recursive_block", "extend_recursive_block", "missing", "block_super",
"wrapper", "extends4", "super_escaped", "extends_chain", "reader_options", "ssi_reader_options",
"extend_doubleblock"]
]},
{"compile_dir",
[setup_compile(T)
|| T <- [#test{
title = "non-existing dir",
source = {dir, "non-existing-made-up-dir"},
renderer = fun(#test{ source={dir, Dir} }) -> Dir end,
output = "non-existing-made-up-dir"
},
#test{
title = "path1",
source = {dir, template_file(input, "path1")},
renderer = fun(#test{ module=M, render_vars=V, render_opts=O }) ->
M:render(base1, V, O)
end
}
]
]}
].
%% {Name, DTL, Vars, Output}
%% {Name, DTL, Vars, RenderOpts, Output}
%% {Name, DTL, Vars, RenderOpts, CompilerOpts, Output}
%% {Name, DTL, Vars, RenderOpts, CompilerOpts, Output, Warnings}
def_to_test(Group, #test{ title=Name }=T) ->
T#test{ title = lists:concat([Group, ": ", Name]) };
def_to_test(Group, {Name, DTL, Vars, Output}) ->
def_to_test(Group, {Name, DTL, Vars, [], [], Output, default_warnings()});
def_to_test(Group, {Name, DTL, Vars, RenderOpts, Output}) ->
def_to_test(Group, {Name, DTL, Vars, RenderOpts, [], Output, default_warnings()});
def_to_test(Group, {Name, DTL, Vars, RenderOpts, CompilerOpts, Output}) ->
def_to_test(Group, {Name, DTL, Vars, RenderOpts, CompilerOpts, Output, default_warnings()});
def_to_test(Group, {Name, DTL, Vars, RenderOpts, CompilerOpts, Output, Warnings}) ->
#test{
title = lists:concat([Group, ": ", Name]),
source = {template, DTL},
render_vars = Vars,
render_opts = RenderOpts,
compile_vars = undefined,
compile_opts = CompilerOpts ++ (#test{})#test.compile_opts,
output = Output,
warnings = Warnings
}.
date_translation(Val, LC) when is_list(Val) ->
io:format("Translating ~p~n", [Val]),
date_translation(list_to_binary(Val),LC);
% date a
date_translation(<<"p.m.">>, <<"ru">>) ->
<<"п.п."/utf8>>;
% date A
date_translation(<<"PM">>, <<"ru">>) ->
<<"ПП"/utf8>>;
% date b
date_translation(<<"jul">>, <<"ru">>) ->
<<"июл"/utf8>>;
% date D
date_translation(<<"Thu">>, <<"ru">>) ->
<<"Чтв"/utf8>>;
% date E
date_translation(<<"July">>, {<<"ru">>, <<"alt. month">>}) ->
<<"ИюлÑ"/utf8>>;
% date F
date_translation(<<"July">>, <<"ru">>) ->
<<"Июль"/utf8>>;
% date l
date_translation(<<"Thursday">>, <<"ru">>) ->
<<"Четверг"/utf8>>;
% date M
date_translation(<<"Sep">>, <<"ru">>) ->
<<"Сен"/utf8>>;
% date N
date_translation(<<"Sept.">>, {<<"ru">>, <<"abbrev. month">>}) ->
<<"Сен."/utf8>>;
% date P
date_translation(<<"noon">>, <<"ru">>) ->
<<"полдень"/utf8>>;
date_translation(Text, <<"ru">>) ->
proplists:get_value(Text,
lists:zip(
lists:map(fun list_to_binary/1, en_months()),
ru_months()),
Text);
date_translation(Text, _) ->
Text.
ru_months() -> [ <<"Январь"/utf8>>, <<"Февраль"/utf8>>, <<"Март"/utf8>>, <<"Ðпрель"/utf8>>,
<<"Май"/utf8>>, <<"Июнь"/utf8>>, <<"Июль"/utf8>>, <<"ÐвгуÑÑ‚"/utf8>>, <<"СентÑбрь"/utf8>>,
<<"ОктÑбрь"/utf8>>, <<"ÐоÑбрь"/utf8>>, <<"Декабрь"/utf8>>].
en_months() -> ["January", "February", "March", "April",
"May", "June", "July", "August", "September",
"October", "November", "December"].
generate_test_date() ->
generate_test_date(false).
generate_test_date(Translation) ->
{{Y,M,D}, _} = erlang:localtime(),
MonthName = case Translation of
russian -> ru_months();
_ -> en_months()
end,
OrdinalSuffix = [
"st","nd","rd","th","th","th","th","th","th","th", % 1-10
"th","th","th","th","th","th","th","th","th","th", % 10-20
"st","nd","rd","th","th","th","th","th","th","th", % 20-30
"st"
],
list_to_binary([
"It is the ",
integer_to_list(D),
lists:nth(D, OrdinalSuffix),
" of ", lists:nth(M, MonthName),
" ", integer_to_list(Y), "."
]).
default_warnings() -> [].
error_info(File, Ws, Mod) ->
{File, [error_info(W, Mod) || W <- Ws]}.
error_info({Line, ErrorDesc}, Mod)
when is_integer(Line); Line =:= none ->
{Line, Mod, ErrorDesc};
error_info({Line, Module, _}=ErrorDesc, _Mod)
when is_integer(Line), is_atom(Module) ->
ErrorDesc;
error_info({none, Module, _}=ErrorDesc, _Mod)
when is_atom(Module) ->
ErrorDesc;
error_info({{Line, Col}, Module, _}=ErrorDesc, _Mod)
when is_integer(Line), is_integer(Col), is_atom(Module) ->
ErrorDesc;
error_info(Ws, Mod) when is_list(Ws) ->
error_info("erly_test", Ws, Mod);
error_info(ErrorDesc, Mod) ->
{none, Mod, ErrorDesc}.
error_info(Ei) ->
error_info(Ei, erlydtl_beam_compiler).
template_file(Dir, Name) ->
%% {ok, CWD} = file:get_cwd(),
%% io:format(user, "~s~n", [CWD]),
filename:join(["test/files", Dir, Name]).
functional_test(F) ->
setup_compile(#test{
title = F,
module = list_to_atom("functional_test_" ++ F),
source = {file, template_file(input, F)}
}).
setup_compile(#test{ title=F, compile_opts=Opts }=T) ->
CompileOpts = [{doc_root, "test/files/input"}|Opts],
case setup_compile(F) of
{ok, [CV|Other]} ->
CO = proplists:get_value(compile_opts, Other, []),
Ws = proplists:get_value(warnings, Other, []),
setup(T#test{
compile_vars = CV,
compile_opts = CO ++ CompileOpts,
warnings = Ws
});
{error, Es, Ws} ->
T#test{
errors = Es,
warnings = Ws,
compile_opts = CompileOpts
}
end;
setup_compile("for_list_preset") ->
CompileVars = [{fruit_list, [["apple", "apples"], ["banana", "bananas"], ["coconut", "coconuts"]]}],
{ok, [CompileVars]};
setup_compile("for_preset") ->
CompileVars = [{fruit_list, ["preset-apple", "preset-banana", "preset-coconut"]}],
{ok, [CompileVars]};
setup_compile("for_records_preset") ->
Link1a = [{name, "Amazon (preset)"}, {url, "http://amazon.com"}],
Link2a = [{name, "Google (preset)"}, {url, "http://google.com"}],
Link3a = [{name, "Microsoft (preset)"}, {url, "http://microsoft.com"}],
CompileVars = [{software_links, [Link1a, Link2a, Link3a]}],
{ok, [CompileVars]};
setup_compile("if_preset") ->
CompileVars = [{var1, "something"}],
{ok, [CompileVars]};
setup_compile("ifequal_preset") ->
CompileVars = [{var1, "foo"}, {var2, "foo"}],
{ok, [CompileVars]};
setup_compile("ifnotequal_preset") ->
CompileVars = [{var1, "foo"}, {var2, "foo"}],
{ok, [CompileVars]};
setup_compile("var_preset") ->
CompileVars = [{preset_var1, "preset-var1"}, {preset_var2, "preset-var2"}],
{ok, [CompileVars]};
setup_compile("extends_for") ->
CompileVars = [{veggie_list, ["broccoli", "beans", "peas", "carrots"]}],
{ok, [CompileVars]};
setup_compile("extends2") ->
File = template_file(input, "extends2"),
Error = {none, erlydtl_beam_compiler, unexpected_extends_tag},
{error, [{File, [Error]}], []};
setup_compile("extends3") ->
File = template_file(input, "extends3"),
Include = template_file(input, "imaginary"),
Error = {none, erlydtl_beam_compiler, {read_file, Include, enoent}},
{error, [{File, [Error]}], []};
setup_compile("extends4") ->
File = template_file(input, "extends4"),
Warning = {{1,21}, erlydtl_beam_compiler, non_block_tag},
{ok, [[]|[{warnings, [{File, [Warning]}]}]]};
setup_compile("missing") ->
File = template_file(input, "missing"),
Error = {none, erlydtl_compiler, {read_file, File, enoent}},
{error, [{File, [Error]}], []};
setup_compile("custom_tag") ->
{ok, [[]|[{compile_opts, [{custom_tags_modules, [erlydtl_custom_tags]}]}]]};
setup_compile("custom_tag1") -> setup_compile("custom_tag");
setup_compile("custom_tag2") -> setup_compile("custom_tag");
setup_compile("custom_tag3") -> setup_compile("custom_tag");
setup_compile("custom_tag4") -> setup_compile("custom_tag");
setup_compile("custom_tag_var") -> setup_compile("custom_tag");
setup_compile("custom_tag_lib_var") ->
{ok, [[]|[{compile_opts, [{libraries, [{custom_tag_lib,erlydtl_custom_tags_lib}]}, {default_libraries, [custom_tag_lib]}]}]]};
setup_compile("super_escaped") ->
{ok, [[]|[{compile_opts, [auto_escape]}]]};
setup_compile("reader_options") ->
{ok, [[]|[{compile_opts, [{reader, {?MODULE, extra_reader}}, {reader_options, [{user_id, <<"007">>}, {user_name, <<"Agent">>}]}]}]]};
setup_compile("ssi_reader_options") ->
{ok, [[]|[{compile_opts, [{reader, {?MODULE, extra_reader}}, {reader_options, [{user_id, <<"007">>}, {user_name, <<"Agent">>}]}]}]]};
%%setup_compile("path1") ->
%% {ok, [[]|[{compile_opts, [debug_compiler]}]]};
setup_compile(_) ->
{ok, [[]]}.
extra_reader(FileName, ReaderOptions) ->
UserID = proplists:get_value(user_id, ReaderOptions, <<"IDUnknown">>),
UserName = proplists:get_value(user_name, ReaderOptions, <<"NameUnknown">>),
case file:read_file(FileName) of
{ok, Data} when UserID == <<"007">>, UserName == <<"Agent">> ->
{ok, Data};
{ok, _Data} ->
{error, "Not Found"};
Err ->
Err
end.
expected(File) ->
Filename = template_file(expect, File),
case file:read_file(Filename) of
{ok, Data} -> Data;
_ -> fun (Data) ->
ok = file:write_file(Filename, Data),
io:format(
user,
"## Saved expected output for test ~p to ~p.~n"
" Verify the contents, as it is used to pass the test on subsequent test runs.~n"
"~n",
[File, Filename]),
throw({verify_new_expected_output, Filename})
end
end.
setup(#test{ title = F, output=undefined }=T) ->
{Vars, Opts, Result} =
case setup(F) of
{ok, V} -> {V, [], expected(F)};
{ok, V, O} -> {V, O, expected(F)};
{ok, V, O, skip_check} -> {V, O, fun (_) -> ok end};
{ok, V, O, R} -> {V, O, R}
end,
T#test{
render_vars = Vars,
render_opts = Opts,
output = Result
};
setup(#test{}=T) -> T;
setup("autoescape") ->
RenderVars = [{var1, "bold"}],
{ok, RenderVars};
setup("extends") ->
RenderVars = [{base_var, "base-barstring"}, {test_var, "test-barstring"}],
{ok, RenderVars};
setup("include_template") -> setup("extends");
setup("include_path") -> setup("extends");
setup("extends_path") -> setup("extends");
setup("extends_path2") -> setup("extends");
setup("block_super") -> setup("extends");
setup("filters") ->
RenderVars = [
{date_var1, {1975,7,24}},
{datetime_var1, {{1975,7,24}, {7,13,1}}},
{'list', ["eins", "zwei", "drei"]}
],
{ok, RenderVars};
setup("for") ->
RenderVars = [{fruit_list, ["apple", "banana", "coconut"]}],
{ok, RenderVars};
setup("for_list") ->
RenderVars = [{fruit_list, [["apple", "apples", "$1"], ["banana", "bananas", "$2"], ["coconut", "coconuts", "$500"]]}],
{ok, RenderVars};
setup("for_tuple") ->
RenderVars = [{fruit_list, [{"apple", "apples"}, {"banana", "bananas"}, {"coconut", "coconuts"}]}],
{ok, RenderVars};
setup("for_records") ->
Link1 = [{name, "Amazon"}, {url, "http://amazon.com"}],
Link2 = [{name, "Google"}, {url, "http://google.com"}],
Link3 = [{name, "Microsoft"}, {url, "http://microsoft.com"}],
RenderVars = [{link_list, [Link1, Link2, Link3]}],
{ok, RenderVars};
setup("for_records_preset") ->
Link1b = [{name, "Canon"}, {url, "http://canon.com"}],
Link2b = [{name, "Leica"}, {url, "http://leica.com"}],
Link3b = [{name, "Nikon"}, {url, "http://nikon.com"}],
RenderVars = [{photo_links, [Link1b, Link2b, Link3b]}],
{ok, RenderVars};
setup("include") ->
RenderVars = [{var1, "foostring1"}, {var2, "foostring2"}],
{ok, RenderVars};
setup("if") ->
RenderVars = [{var1, "something"}],
{ok, RenderVars};
setup("ifequal") ->
RenderVars = [{var1, "foo"}, {var2, "foo"}, {var3, "bar"}],
{ok, RenderVars};
setup("ifequal_preset") ->
RenderVars = [{var3, "bar"}],
{ok, RenderVars};
setup("ifnotequal") ->
RenderVars = [{var1, "foo"}, {var2, "foo"}, {var3, "bar"}],
{ok, RenderVars};
setup("now") ->
{ok, [], [], skip_check};
setup("var") ->
RenderVars = [{var1, "foostring1"}, {var2, "foostring2"}, {var_not_used, "foostring3"}],
{ok, RenderVars};
setup("var_preset") ->
RenderVars = [{var1, "foostring1"}, {var2, "foostring2"}],
{ok, RenderVars};
setup("cycle") ->
RenderVars = [{test, [integer_to_list(X) || X <- lists:seq(1, 20)]},
{a, "Apple"}, {b, "Banana"}, {c, "Cherry"}],
{ok, RenderVars};
setup("trans") ->
RenderOpts = [{translation_fun, fun lists:reverse/1}],
{ok, [], RenderOpts};
setup("locale") ->
{ok, _RenderVars = [{locale, "ru"}]};
setup("custom_tag1") ->
{ok, [{a, <<"a1">>}], [{locale, ru}], <<"b1\n">>};
setup("custom_tag2") ->
{ok, [{a, <<"a1">>}], [{locale, ru}, {foo, bar}], <<"b2\n">>};
setup("custom_tag3") ->
{ok, [{a, <<"a1">>}], [{locale, ru}], <<"b3\n">>};
setup("custom_tag4") ->
{ok, [], [], <<"a\n">>};
setup("custom_tag_var") ->
{ok, [{a, <<"a1">>}], [{locale, ru}], <<"\nb1\n11\n">>};
setup("custom_tag_lib_var") ->
{ok, [{a, <<"a1">>}], [{locale, ru}], <<"\nb1\n11\n">>};
setup("ssi") ->
RenderVars = [{path, "ssi_include.html"}],
{ok, RenderVars};
setup("wrapper") ->
RenderVars = [{types, ["b", "a", "c"]}],
{ok, RenderVars};
setup("reader_options") ->
RenderVars = [{base_var, "base-barstring"}, {test_var, "test-barstring"}],
% Options = [],%[{compile_opts, [{reader, {?MODULE, extra_reader}}, {reader_options, [{user_id, <<"007">>}, {user_name, <<"Agent">>}]}]}],
{ok, RenderVars};
setup("ssi_reader_options") ->
RenderVars = [{path, "ssi_include.html"}],
{ok, RenderVars};
%%--------------------------------------------------------------------
%% Custom tags
%%--------------------------------------------------------------------
setup("custom_call") ->
RenderVars = [{var1, "something"}],
{ok, RenderVars};
setup(_) ->
{ok, []}.
erlydtl-0.15.0/test/erlydtl_test_extension.erl 0000664 0000000 0000000 00000002633 15047124314 0021556 0 ustar 00root root 0000000 0000000 -module(erlydtl_test_extension).
-export([scan/1, parse/1, compile_ast/2]).
-include("erlydtl_ext.hrl").
%% look for a foo identifier followed by a #
scan(#scanner_state{ template="#" ++ T,
scanned=[{identifier, Loc, foo}|Scanned],
pos={L,C} }=S) ->
%% return new state with the hash dropped, and the foo identifier replaced with bar
{ok, S#scanner_state{ template=T,
scanned=[{identifier, Loc, "rab"}|Scanned],
pos={L, C+1} }};
scan(#scanner_state{ template="#" ++ _T, pos=Pos }) ->
%% give error when # not follows foo
{error, {Pos,erlydtl_scanner,{illegal_char, $#}}};
scan(_) ->
%% for anything else, fallback to the error message from erlydtl_scanner..
undefined.
parse(State) ->
erlydtl_extension_testparser:resume(State).
%% {{ varA or varB }} is equivalent to {% if varA %}{{ varA }}{% else %}{{ varB }}{% endif %}
compile_ast({value_or, {Value1, Value2}}, TreeWalker) ->
{{V1_Ast, V1_Info}, TW1} = erlydtl_beam_compiler:value_ast(Value1, false, false, TreeWalker),
{{V2_Ast, V2_Info}, TW2} = erlydtl_beam_compiler:value_ast(Value2, false, false, TW1),
{{erl_syntax:case_expr(V1_Ast,
[erl_syntax:clause([erl_syntax:atom(undefined)], none, [V2_Ast]),
erl_syntax:clause([erl_syntax:underscore()], none, [V1_Ast])
]), erlydtl_compiler_utils:merge_info(V1_Info, V2_Info)}, TW2}.
erlydtl-0.15.0/test/erlydtl_tests.erl 0000664 0000000 0000000 00000003342 15047124314 0017643 0 ustar 00root root 0000000 0000000 %%%-------------------------------------------------------------------
%%% File: erlydtl_tests.erl
%%% @author Andreas Stenius
%%% @copyright 2014 Andreas Stenius
%%% @doc
%%% Test suite for erlydtl
%%% @end
%%%
%%% The MIT License
%%%
%%% Copyright (c) 2014 Andreas Stenius
%%%
%%% 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.
%%%
%%% @since 2014 by Andreas Stenius
%%%-------------------------------------------------------------------
-module(erlydtl_tests).
-author('Andreas Stenius ').
-include_lib("eunit/include/eunit.hrl").
-include("testrunner.hrl").
all_defs_test_() ->
[{T#test.title,
fun () -> run_test(T) end}
|| T <- erlydtl_test_defs:tests()
].
erlydtl-0.15.0/test/erlydtl_translation_tests.erl 0000664 0000000 0000000 00000010234 15047124314 0022257 0 ustar 00root root 0000000 0000000 -module(erlydtl_translation_tests).
-include_lib("eunit/include/eunit.hrl").
all_sources_parser_test_() ->
[{Title, [test_fun(Test) || Test <- Tests]}
|| {Title, Tests} <- test_defs()].
test_fun({Name, Template, Variables, Options, Output}) ->
{Name, fun () ->
Tokens = (catch compile_and_render(Template, Variables, Options)),
?assertMatch(Output, Tokens)
end}.
compile_and_render(Template, Variables, Options) ->
{ok, test} = erlydtl:compile_template(Template, test),
{ok, R} = test:render(Variables, Options),
iolist_to_binary(R).
test_defs() ->
[
{"trans", [
{"simple", "{% trans \"hello\" %}", [], [], <<"hello">>},
{"with_fun", "{% trans \"text\" %}", [], [{translation_fun, fun(_ID, _L) -> "hola" end}], <<"hola">>},
{"with_fun_utf8", "{% trans \"text\" %}", [],
[{translation_fun, fun(_ID, _L) -> <<"привет"/utf8>> end}], <<"привет"/utf8>>}
]},
{"blocktrans", [
{"simple", "{% blocktrans %} hello {% endblocktrans %}", [], [], <<" hello ">>},
{"with_fun", "{% blocktrans %} hello {% endblocktrans %}", [],
[{translation_fun, fun(_ID, _L) -> "hola" end}], <<"hola">>},
{"s_param_no_fun", "{% blocktrans %} hello {{ p }} {% endblocktrans %}", [{p, "mundo"}],
[], <<" hello mundo ">>},
{"s_param", "{% blocktrans %} hello {{ p }} {% endblocktrans %}", [{p, "mundo"}],
[{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}], <<"hola mundo">>},
{"b_param", "{% blocktrans %} hello {{ p }} {% endblocktrans %}", [{p, <<"mundo">>}],
[{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}], <<"hola mundo">>},
{"i_param", "{% blocktrans %} hello {{ p }} {% endblocktrans %}", [{p, 1}],
[{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}], <<"hola 1">>},
{"f_param", "{% blocktrans %} hello {{ p }} {% endblocktrans %}", [{p, 3.1415}],
[{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}], <<"hola 3.1415">>},
{"b_xss", "{% blocktrans %} hello {{ p }} {% endblocktrans %}",
[{p, <<"">>}],
[{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}],
<<"hola <script>alert('pwnd');</script>">>},
{"s_xss", "{% blocktrans %} hello {{ p }} {% endblocktrans %}",
[{p, ""}],
[{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}],
<<"hola <script>alert('pwnd');</script>">>},
{"b_autoecape_off",
"{% autoescape off %}{% blocktrans %} hello {{ p }} {% endblocktrans %}{% endautoescape %}",
[{p, <<"">>}],
[{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}],
<<"hola ">>},
{"b_autoecape_nested",
"{% autoescape off %}{% autoescape on %}{% blocktrans %} hello {{ p }} {% endblocktrans %}{% endautoescape %}{% endautoescape %}",
[{p, <<"">>}],
[{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}],
<<"hola <script>alert('pwnd');</script>">>},
{"term_hack_", "{% blocktrans %} hello {{ p }} {% endblocktrans %}",
[{p, {""}}],
[{translation_fun, fun(_ID, _L) -> "hola {{ p }}" end}],
<<"hola {"<script>alert('pwnd');</script>"}">>},
{"plural_2",
"{% blocktrans count counter=p %} hello world {% plural %} hello {{ p }} worlds {% endblocktrans %}",
[{p, 2}],
[{translation_fun, fun({" hello world ", {" hello {{ p }} worlds ", 2}}, _L) ->
"hola {{ p }} mundos"
end}],
<<"hola 2 mundos">>}
]}
].
erlydtl-0.15.0/test/files/ 0000775 0000000 0000000 00000000000 15047124314 0015336 5 ustar 00root root 0000000 0000000 erlydtl-0.15.0/test/files/expect/ 0000775 0000000 0000000 00000000000 15047124314 0016626 5 ustar 00root root 0000000 0000000 erlydtl-0.15.0/test/files/expect/autoescape 0000664 0000000 0000000 00000000214 15047124314 0020677 0 ustar 00root root 0000000 0000000
This is escaped: <b>bold</b>
This is not escaped: bold
This is escaped: <b>bold</b>
erlydtl-0.15.0/test/files/expect/block_super 0000664 0000000 0000000 00000000303 15047124314 0021055 0 ustar 00root root 0000000 0000000 base-barstring
base template
extending title: "base title"
more of base template
replacing the base content - variable: test-barstring after variable. Was: base content
end of base template
erlydtl-0.15.0/test/files/expect/comment 0000664 0000000 0000000 00000000456 15047124314 0020220 0 ustar 00root root 0000000 0000000
Test Comment
bla
blue
black
erlydtl-0.15.0/test/files/expect/custom_call 0000664 0000000 0000000 00000000457 15047124314 0021064 0 ustar 00root root 0000000 0000000 >>>> before custom call tag 'comment'
before
preset-apple
preset-banana
preset-coconut
after
>>>> after custom call tag 'comment'
>>>> before custom call tag 'if'
One but not two: two
Two but not one: one
One:
None:
>>>> after custom call tag 'if'
erlydtl-0.15.0/test/files/expect/custom_tag 0000664 0000000 0000000 00000002250 15047124314 0020715 0 ustar 00root root 0000000 0000000
Test variable
before
To view the Video:
after
erlydtl-0.15.0/test/files/expect/cycle 0000664 0000000 0000000 00000001012 15047124314 0017642 0 ustar 00root root 0000000 0000000 before
1. 1 - Apple
2. 2 - Banana
3. 3 - Cherry
4. 4 - Apple
5. 5 - Banana
6. 6 - Cherry
7. 7 - Apple
8. 8 - Banana
9. 9 - Cherry
10. 10 - Apple
11. 11 - Banana
12. 12 - Cherry
13. 13 - Apple
14. 14 - Banana
15. 15 - Cherry
16. 16 - Apple
17. 17 - Banana
18. 18 - Cherry
19. 19 - Apple
20. 20 - Banana
after
erlydtl-0.15.0/test/files/expect/extend_doubleblock 0000664 0000000 0000000 00000000176 15047124314 0022411 0 ustar 00root root 0000000 0000000
Chicago Boss Admin - General Info
Chicago Boss Admin - General Info
erlydtl-0.15.0/test/files/expect/extend_recursive_block 0000664 0000000 0000000 00000000027 15047124314 0023300 0 ustar 00root root 0000000 0000000
extended content
erlydtl-0.15.0/test/files/expect/extends 0000664 0000000 0000000 00000000254 15047124314 0020224 0 ustar 00root root 0000000 0000000 base-barstring
base template
replacing the base title
more of base template
replacing the base content - variable: test-barstring after variable
end of base template
erlydtl-0.15.0/test/files/expect/extends4 0000664 0000000 0000000 00000000127 15047124314 0020307 0 ustar 00root root 0000000 0000000
base template
base title
more of base template
base content
end of base template
erlydtl-0.15.0/test/files/expect/extends_chain 0000664 0000000 0000000 00000000142 15047124314 0021362 0 ustar 00root root 0000000 0000000
after
erlydtl-0.15.0/test/files/expect/extends_path 0000664 0000000 0000000 00000000371 15047124314 0021240 0 ustar 00root root 0000000 0000000 base-barstring
base2 template
replacing the base title
block title 2 from base1
more of base2 template
replacing the base content - variable: test-barstring after variable
block content2 in base 2, should pass through
end of base2 template
erlydtl-0.15.0/test/files/expect/extends_path2 0000664 0000000 0000000 00000000101 15047124314 0021311 0 ustar 00root root 0000000 0000000 pre content
start_content
This is include1
end_content
post
erlydtl-0.15.0/test/files/expect/filters 0000664 0000000 0000000 00000001040 15047124314 0020214 0 ustar 00root root 0000000 0000000
Add: 2 + 2 = 4
Capfirst: Capitalized
Centered:
center
Date format: Thu, 24 Jul 1975
DateTime format: Thu, 24 Jul 1975 07:13:01
Escape JS: \u0022 \u0027
First letter: f
Fix ampersands: &
Force_escape: <b></b>
Joined: eins, zwei, drei
Last: t
Length: 3
Length is 2?: false
Left adjust:
left
Line breaks: Line 1 Line 2 Line 3
Lowercase: lowercase
Right adjust:
after
erlydtl-0.15.0/test/files/expect/for_list 0000664 0000000 0000000 00000000256 15047124314 0020375 0 ustar 00root root 0000000 0000000
More than one apple is called "apples". Only $1 each!
More than one banana is called "bananas". Only $2 each!
More than one coconut is called "coconuts". Only $500 each!
erlydtl-0.15.0/test/files/expect/for_list_preset 0000664 0000000 0000000 00000000202 15047124314 0021746 0 ustar 00root root 0000000 0000000
More than one apple is called "apples".
More than one banana is called "bananas".
More than one coconut is called "coconuts".
erlydtl-0.15.0/test/files/expect/for_preset 0000664 0000000 0000000 00000000145 15047124314 0020721 0 ustar 00root root 0000000 0000000 before
preset-apple
preset-banana
preset-coconut
after erlydtl-0.15.0/test/files/expect/for_records 0000664 0000000 0000000 00000000266 15047124314 0021064 0 ustar 00root root 0000000 0000000 before
after erlydtl-0.15.0/test/files/expect/for_tuple 0000664 0000000 0000000 00000000117 15047124314 0020547 0 ustar 00root root 0000000 0000000
One apple, two apples!
One banana, two bananas!
One coconut, two coconuts!
erlydtl-0.15.0/test/files/expect/if 0000664 0000000 0000000 00000000100 15047124314 0017136 0 ustar 00root root 0000000 0000000 One but not two: one
Two but not one: two
One: one
None:
erlydtl-0.15.0/test/files/expect/if_preset 0000664 0000000 0000000 00000000100 15047124314 0020520 0 ustar 00root root 0000000 0000000 One but not two: one
Two but not one: two
One: one
None:
erlydtl-0.15.0/test/files/expect/ifequal 0000664 0000000 0000000 00000000413 15047124314 0020175 0 ustar 00root root 0000000 0000000
if: var1="foo" and var2="foo" are equal
if: var1="foo" and var2="foo" are equal
else: var1="foo" and var3="bar" are not equal
if: "foo" and "foo" are equal
else: "foo" and "bar" are not equal
if: 99 and 99 are equal
else: 77 and 99 are not equal
erlydtl-0.15.0/test/files/expect/ifequal_preset 0000664 0000000 0000000 00000000413 15047124314 0021557 0 ustar 00root root 0000000 0000000
if: var1="foo" and var2="foo" are equal
if: var1="foo" and var2="foo" are equal
else: var1="foo" and var3="bar" are not equal
if: "foo" and "foo" are equal
else: "foo" and "bar" are not equal
if: 99 and 99 are equal
else: 77 and 99 are not equal
erlydtl-0.15.0/test/files/expect/ifnotequal 0000664 0000000 0000000 00000000413 15047124314 0020716 0 ustar 00root root 0000000 0000000
else: var1="foo" and var2="foo" are not equal
if: var1="foo" and var3="bar" are equal
if: var1="foo" and var3="bar" are equal
else: "foo" and "foo" are not equal
if: "foo" and "bar" are equal
else: 99 and 99 are not equal
if: 77 and 99 are equal
erlydtl-0.15.0/test/files/expect/ifnotequal_preset 0000664 0000000 0000000 00000000413 15047124314 0022300 0 ustar 00root root 0000000 0000000
else: var1="foo" and var2="foo" are not equal
if: var1="foo" and var3="bar" are equal
if: var1="foo" and var3="bar" are equal
else: "foo" and "foo" are not equal
if: "foo" and "bar" are equal
else: 99 and 99 are not equal
if: 77 and 99 are equal
erlydtl-0.15.0/test/files/expect/include 0000664 0000000 0000000 00000000066 15047124314 0020176 0 ustar 00root root 0000000 0000000 Including another file: This is included! foostring1
erlydtl-0.15.0/test/files/expect/include_path 0000664 0000000 0000000 00000000126 15047124314 0021207 0 ustar 00root root 0000000 0000000 main file
This is template 1.
test-barstring
This is template 2
base-barstring
erlydtl-0.15.0/test/files/expect/include_template 0000664 0000000 0000000 00000000241 15047124314 0022064 0 ustar 00root root 0000000 0000000 Including another template: base-barstring
base template
base title
more of base template
base content
end of base template
test variable: test-barstring
erlydtl-0.15.0/test/files/expect/path1 0000664 0000000 0000000 00000000363 15047124314 0017570 0 ustar 00root root 0000000 0000000
base2 template
block title in base2, should be hidden by main template
block title 2 from base1
more of base2 template
block content in base 2, should be overwritten
block content2 in base 2, should pass through
end of base2 template
erlydtl-0.15.0/test/files/expect/reader_options 0000664 0000000 0000000 00000000145 15047124314 0021566 0 ustar 00root root 0000000 0000000 base-barstring
base template
base title
more of base template
base content
end of base template
erlydtl-0.15.0/test/files/expect/recursive_block 0000664 0000000 0000000 00000000074 15047124314 0021733 0 ustar 00root root 0000000 0000000
testing:
erlydtl-0.15.0/test/files/expect/trans 0000664 0000000 0000000 00000000017 15047124314 0017676 0 ustar 00root root 0000000 0000000 gnirtS elpmaxE
erlydtl-0.15.0/test/files/expect/var 0000664 0000000 0000000 00000000136 15047124314 0017341 0 ustar 00root root 0000000 0000000 before varriable1
foostring1
after variable1
foostring2
after variable2 (HTML-comment-wrapped) erlydtl-0.15.0/test/files/expect/var_preset 0000664 0000000 0000000 00000000106 15047124314 0020720 0 ustar 00root root 0000000 0000000 one
foostring1
two
preset-var1
three
foostring2
four
preset-var2
five
erlydtl-0.15.0/test/files/expect/wrapper 0000664 0000000 0000000 00000000615 15047124314 0020233 0 ustar 00root root 0000000 0000000
including base b now
base template
base b
more of base template
base content
end of base template
including base a now
base template
base a
more of base template
base content
end of base template
including base c now
base template
base c
more of base template
base content
end of base template
erlydtl-0.15.0/test/files/input/ 0000775 0000000 0000000 00000000000 15047124314 0016475 5 ustar 00root root 0000000 0000000 erlydtl-0.15.0/test/files/input/autoescape 0000664 0000000 0000000 00000000305 15047124314 0020547 0 ustar 00root root 0000000 0000000 {% autoescape on %}
This is escaped: {{ var1 }}
{% autoescape off %}
This is not escaped: {{ var1 }}
This is escaped: {{ var1|escape }}
{% endautoescape %}
{% endautoescape %}
erlydtl-0.15.0/test/files/input/base 0000775 0000000 0000000 00000000245 15047124314 0017336 0 ustar 00root root 0000000 0000000 {{ base_var }}
base template
{% block title %}base title{% endblock %}
more of base template
{% block content %}base content{% endblock %}
end of base template
erlydtl-0.15.0/test/files/input/base_a 0000664 0000000 0000000 00000000074 15047124314 0017633 0 ustar 00root root 0000000 0000000 {% extends "base" %}
{% block title %}base a{% endblock %}
erlydtl-0.15.0/test/files/input/base_b 0000664 0000000 0000000 00000000074 15047124314 0017634 0 ustar 00root root 0000000 0000000 {% extends "base" %}
{% block title %}base b{% endblock %}
erlydtl-0.15.0/test/files/input/base_c 0000664 0000000 0000000 00000000074 15047124314 0017635 0 ustar 00root root 0000000 0000000 {% extends "base" %}
{% block title %}base c{% endblock %}
erlydtl-0.15.0/test/files/input/base_doubleblock 0000664 0000000 0000000 00000000244 15047124314 0021677 0 ustar 00root root 0000000 0000000
Chicago Boss Admin - {% block title %}{% endblock %}
Chicago Boss Admin - {% block title %}{% endblock %}