Skip to content

Commit

Permalink
Function calls (#151)
Browse files Browse the repository at this point in the history
* WIP

* try to match function handling of docs

* commit to this

* string

* fix for 1.0.5

* fix for 1.0.5
  • Loading branch information
jverzani authored Mar 17, 2023
1 parent d71b8b4 commit 5ebc817
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 32 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
*.jl.mem
docs/build
docs/site
docs/logo.jl
Manifest.toml
.travis.yml
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "Mustache"
uuid = "ffc61752-8dc7-55ee-8c37-f3e9cdd09e70"
version = "1.0.14"
version = "1.0.15"

[deps]
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![Build Status](https://github.com/jverzani/Mustache.jl/workflows/CI/badge.svg)](https://github.com/jverzani/Mustache.jl/actions)
[![codecov](https://codecov.io/gh/jverzani/Mustache.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/jverzani/Mustache.jl)

[Mustache](http://mustache.github.io/) is
[{{ mustache }}](http://mustache.github.io/) is

... a logic-less template syntax. It can be used for HTML,
config files, source code - anything. It works by expanding tags in a
Expand Down
50 changes: 45 additions & 5 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Mustache.render(goes_together, x="Salt", y="pepper")
Mustache.render(goes_together, x="Bread", y="butter")
```

Keyword arguments can also be passed to a `Tokens` object directly (bypassing the use of `render`):
`Tokens` objects are functors; keyword arguments can also be passed to a `Tokens` object directly (bypassing the use of `render`):

```julia
goes_together = mt"{{{:x}}} and {{{:y}}}."
Expand Down Expand Up @@ -195,7 +195,8 @@ Mustache.render(mt"a {{:b}} c", b = () -> "Bea") # "a Bea c"
```

```julia
Mustache.render(mt"Written in the year {{:yr}}."; yr = yearnow) # "Written in the year 2022."
using Dates
Mustache.render(mt"Written in the year {{:yr}}."; yr = yearnow) # "Written in the year 2023."
```

### Sections
Expand Down Expand Up @@ -229,6 +230,17 @@ specification:
Mustache.render("{{#:a}}one{{/:a}}", a=length) # "3"
```

The specification has been widened to accept functions of two arguments, the string and a render function:

```julia
tpl = mt"{{#:bold}}Hi {{:name}}.{{/:bold}}"
function bold(text, render)
"<b>" * render(text) * "</b>"
end
tpl(; name="Tater", bold=bold) # "<b>Hi Tater.</b>"
```



If the tag "|" is used, the section value will be rendered first, an enhancement to the specification.

Expand Down Expand Up @@ -408,6 +420,31 @@ implemented for now -- does not allow for iteration. That is
constructs like `{{#.[1]}}` don't introduce iteration, but only offer
a conditional check.

### Iterating when the value of a section variable is a function

From the Mustache documentation, consider the template

```julia
tpl = mt"{{#:beatles}}
* {{:name}}
{{/:beatles}}"
```

when `beatles` is a vector of named tuples (or some other `Tables.jl` object) and `name` is a function.

When iterating over `beatles`, `name` can reference the rows of the `beatles` object by name. In `JavaScript`, this is done with `this.XXX`. In `Julia`, the values are stored in the `task_local_storage` object (with symbols as keys) allowing the access. The `Mustache.get_this` function allows `JavaScript`-like usage:

```julia
function name()
this = Mustache.get_this()
this.first * " " * this.last
end
beatles = [(first="John", last="Lennon"), (first="Paul", last="McCartney")]

tpl(; beatles, name) # "* John Lennon\n* Paul McCartney\n"
```


### Conditional checking without iteration

The section tag, `#`, check for existence; pushes the object into the view; and then iterates over the object. For cases where iteration is not desirable; the tag type `@` can be used.
Expand Down Expand Up @@ -492,9 +529,12 @@ To summarize the different tags marking a variable:
`nothing` will use the text between the matching tags, marked
with `{{/variable}}`; otherwise that text will be skipped. (Like
an `if/end` block.)
- if `variable` is a function, it will be applied to contents of the
section. Use of `|` instead of `#` will instruct the rendering of
the contents before applying the function.
- if `variable` is a function, it will be applied to contents of
the section. Use of `|` instead of `#` will instruct the
rendering of the contents before applying the function. The spec
allows for a function to have signature `(x, render)` where `render` is
used internally to convert. This implementation allows rendering
when `(x)` is the single argument.
- if `variable` is a `Tables.jl` compatible object (row wise, with
named rows), will iterate over the values, pushing the named
tuple to be the top-most view for the part of the template up to
Expand Down
5 changes: 3 additions & 2 deletions src/context.jl
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ end
## After checking several specific types, if view is Tables compatible
## this will return "column" corresponding to the key
function lookup_in_view(view, key)

val = _lookup_in_view(view, key)
!falsy(val) && return val

Expand Down Expand Up @@ -171,12 +170,14 @@ _lookup_in_view(view, key) = nothing

## Default lookup is likely not great,
function __lookup_in_view(view, key)

k = normalize(key)
k′ = Symbol(k)

# check propertyname, then fieldnames
if k in propertynames(view)
getproperty(view, k)
elseif k′ in propertynames(view)
getproperty(view, k′)
else

nms = fieldnames(typeof(view))
Expand Down
68 changes: 47 additions & 21 deletions src/tokens.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# XXX
# * if the value of a section variable is a function, it will be called in the context of the current item in the list on each iteration
# * If the value of a section key is a function, it is called with the section's literal block of text, un-rendered, as its first argument. The second argument is a special rendering function that uses the current view as its view argument. It is called in the context of the current view object.


# Token types
abstract type Token end
Expand Down Expand Up @@ -35,7 +39,7 @@ BooleanToken(_type, value, ltag, rtag) = new(_type, value, ltag, rtag, Any[])
end

mutable struct MustacheTokens
tokens::Vector{Token}
tokens::Vector{Token}
end
MustacheTokens() = MustacheTokens(Token[])

Expand All @@ -60,8 +64,6 @@ function Base.push!(tokens::MustacheTokens, token::Token)
end




struct AnIndex
ind::Int
value::String
Expand Down Expand Up @@ -380,7 +382,7 @@ function nestTokens(tokens)
collector = tree
sections = MustacheTokens() #Array{Any}(undef, 0)

for i in 1:length(tokens)
for i 1:length(tokens)
token = tokens[i]
## a {{#name}}...{{/name}} will iterate over name
## a {{^name}}...{{/name}} does ... if we have no name
Expand Down Expand Up @@ -465,9 +467,10 @@ end

function _renderTokensByValue(value::Union{AbstractArray, Tuple}, io, token, writer, context, template, args...)
inverted = token._type == "^"
if (inverted && falsy(value))
renderTokens(io, token.collector, writer, ctx_push(context, ""), template, args...)
else

if (inverted && falsy(value))
renderTokens(io, token.collector, writer, ctx_push(context, ""), template, args...)
else
n = length(value)
for (i,v) in enumerate(value)
renderTokens(io, token.collector, writer, ctx_push(context, v), template, (i,n))
Expand All @@ -479,6 +482,7 @@ end
## what to do with an index value `.[ind]`?
## We have `.[ind]` being of a leaf type (values are not pushed onto a Context) so of simple usage
function _renderTokensByValue(value::AnIndex, io, token, writer, context, template, idx=(0,0))

idx_match = (value.ind == idx[1]) || (value.ind==-1 && idx[1] == idx[2])
if token._type == "#" || token._type == "|"
# print if match
Expand All @@ -505,13 +509,14 @@ function _renderTokensByValue(value::Function, io, token, writer, context, templ
# not have been expanded - the lambda should do that on
# its own. In this way you can implement filters or
# caching.

# out = (value())(token.collector, render)
if token._type == "name"
out = render(value(), context.view)
push_task_local_storage(context.view)
out = render(string(value()), context.view)
elseif token._type == "|"
# pass evaluated values
view = context.parent.view
push_task_local_storage(view)
sec_value = render(MustacheTokens(token.collector), view)
out = render(string(value(sec_value)), view)
else
Expand All @@ -520,9 +525,21 @@ function _renderTokensByValue(value::Function, io, token, writer, context, templ
## Lambdas used for sections should parse with the current delimiters.
sec_value = toString(token.collector)
view = context.parent.view
tpl = string(value(sec_value))
out = render(parse(tpl, (token.ltag, token.rtag)), view)

if isa(value, Function)
## Supposed to be called value(sec_value, render+context) but
## we call render(value(sec_value), context)
push_task_local_storage(view)
_render = x -> render(x, view)
out = try
value(sec_value, _render)
catch err
value(sec_value)
end
else
out = value
end
# ensure tags are used
out = render(parse(string(out), (token.ltag, token.rtag)), view)
end
write(io, out)

Expand All @@ -543,17 +560,13 @@ end
## was contained in that section.
function renderTokens(io, tokens, writer, context, template, idx=(0,0))
for i in 1:length(tokens)

token = tokens[i]
tokenValue = token.value

if token._type == "#" || token._type == "|"

## iterate over value if Dict, Array or DataFrame,
## or display conditionally
value = lookup(context, tokenValue)
ctx = isa(value, AnIndex) ? context : Context(value, context)

renderTokensByValue(value, io, token, writer, ctx, template, idx)

# if !isa(value, AnIndex)
Expand Down Expand Up @@ -644,14 +657,24 @@ function renderTokens(io, tokens, writer, context, template, idx=(0,0))
if !falsy(value)
## desc: A lambda's return value should parse with the default delimiters.
## parse(value()) ensures that
val = isa(value, Function) ? render(parse(value()), context.view) : value
if isa(value, Function)
push_task_local_storage(context.view)
val = render(parse(string(value())), context.view)
else
val = value
end
print(io, val)
end

elseif token._type == "{"
value = lookup(context, tokenValue)
if !falsy(value)
val = isa(value, Function) ? render(parse(value()), context.view) : value
if isa(value, Function)
push_task_local_storage(context.view)
val = render(parse(string(value())), context.view)
else
val = value
end
print(io, val)
end

Expand All @@ -664,9 +687,12 @@ function renderTokens(io, tokens, writer, context, template, idx=(0,0))
elseif token._type == "name"
value = lookup(context, tokenValue)
if !falsy(value)
val = isa(value, Function) ?
render((parsestringvalue)(), context.view) :
value
if isa(value, Function)
push_task_local_storage(context.view)
val = render(string(value()), context.view)
else
val = value
end
print(io, escape_html(val))
end

Expand Down
26 changes: 26 additions & 0 deletions src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,33 @@ function normalize(key)
return key
end

# means to push values into scope of function through
# magic `this` variable, ala JavaScript
# user in function has
# `this = Mustache.get_this()`
# then `this.prop` should get value or nothing
struct This{T}
__v__::T
end
# get
function Base.getproperty(this::This, key::Symbol)
key == :__v__ && return getfield(this, :__v__)
get(this.__v__, key, nothing)
end

# push to task local storage to evaluate function
function push_task_local_storage(view)
task_local_storage(:__this__,This(view))
end

get_this() = get(task_local_storage(), :__this__, This(()))


## hueristic to avoid loading DataFrames
## Once `Tables.jl` support for DataFrames is available, this can be dropped
is_dataframe(x) = !isa(x, Dict) && !isa(x, Module) &&!isa(x, Array) && occursin(r"DataFrame", string(typeof(x)))

## hacky means to evaluate functions
macro gobal(v)
Expr(:global, Expr(:(=), :this, esc(v)))
end
32 changes: 30 additions & 2 deletions test/Mustache_test.jl
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,14 @@ tpl = mt"""{{#wrapped}}
{{/wrapped}}
"""

d = Dict("name" => "Willy", "wrapped" => (txt, r) -> "<b>" * r(txt) * "</b>")
@test Mustache.render(tpl, d) == "<b>Willy is awesome.\n</b>"

# this shouldn't be "Willy", rather "{{name}}"
d = Dict("name" => "Willy", "wrapped" => (txt) -> "<b>" * txt * "</b>")
@test Mustache.render(tpl, d) == "<b>Willy is awesome.\n</b>"


## Test of using Dict in {{#}}/{{/}} things
tpl = mt"{{#:d}}{{x}} and {{y}}{{/:d}}"
d = Dict(); d["x"] = "salt"; d["y"] = "pepper"
Expand All @@ -81,8 +86,6 @@ d = Dict("lambda" => (txt) -> begin
)
@test Mustache.render(tpl, d) == "value dollars."



## test nested section with filtering lambda
tpl = """
{{#lambda}}
Expand All @@ -99,6 +102,31 @@ d = Dict("iterable"=>Dict("iterable2"=>["a","b","c"]), "lambda"=>(txt) -> "XXX $
expected = "XXX a\nb\nc\n XXX"
@test Mustache.render(tpl, d) == expected

## If the value of a section key is a function, it is called with the section's literal block of text, un-rendered, as its first argument. The second argument is a special rendering function that uses the current view as its view argument. It is called in the context of the current view object.
tpl = mt"{{#:bold}}Hi {{:name}}.{{/:bold}}"
function bold(text, render)
this = Mustache.get_this()
"<b>" * render(text) * "</b>" * this.xtra
end
expected = "<b>Hi Tater.</b>Hi Willy."
@test Mustache.render(tpl; name="Tater", bold=bold, xtra = "Hi Willy.") == expected



## if the value of a section variable is a function, it will be called in the context of the current item in the list on each iteration.
tpl = mt"{{#:beatles}}
* {{:name}}
{{/:beatles}}"
function name()
this = Mustache.get_this()
this.first * " " * this.last
end
beatles = [(first="John", last="Lennon"), (first="Paul", last="McCartney")]
expected = "* John Lennon\n* Paul McCartney\n"
@test tpl(; beatles=beatles, name=name) == expected




## Test with Named Tuples as a view
tpl = "{{#:NT}}{{:a}} and {{:b}}{{/:NT}}"
Expand Down

2 comments on commit 5ebc817

@jverzani
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/79792

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v1.0.15 -m "<description of version>" 5ebc8172ca56e79b35cc86e1bd3568a802127c91
git push origin v1.0.15

Please sign in to comment.