Skip to content

Commit

Permalink
close issue #139 (#140)
Browse files Browse the repository at this point in the history
  • Loading branch information
jverzani authored Nov 9, 2021
1 parent 39d7584 commit 94fc3f7
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 147 deletions.
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
name = "Mustache"
uuid = "ffc61752-8dc7-55ee-8c37-f3e9cdd09e70"
version = "1.0.11"
version = "1.0.12"

[deps]
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"

[compat]
Tables = "0.2, 1"
Tables = "1"
julia = "1.0"

[extras]
Expand Down
5 changes: 3 additions & 2 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -469,15 +469,16 @@ To summarize the different tags marking a variable:

* `{{variable}}` does substitution of the value held in `variable` in the current view; escapes HTML characters
* `{{{variable}}}` does substitution of the value held in `variable` in the current view; does not escape HTML characters. The mustache braces can be adjusted using `Mustache.parse`.
* `{{-variable}}` does substitution of the value held in `variable` in the outmost view
* `{{&variable}}` is an alternative syntax for triple braces (useful with custom braces)
* `{{~variable}}` does substitution of the value held in `variable` in the outmost view
* `{{#variable}}` depending on the type of variable, does the following:
- if `variable` is not a container and is not absent or `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 `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 `{{\variable}}`.
- if `variable` is a vector or tuple -- for the part of the template up to `{{\variable}}` -- will iterate over the values. Use `{{.}}` to refer to the (unnamed) values. The values `.[end]` and `.[i]`, for a numeric literal, will refer to values in the vector or tuple.
* `{{^variable}}`/`{{.variable}}` tags will show the values when `variable` is not defined, or is `nothing`.
* `{{>partial}}` will include the partial value into the template, filling in the template using the current view. The partial can be a variable or a filename (checked with `isfile`).
* `{{<partial}}` directly include partial value into template without filling in with the current view.

* `{{!comment}}}` comments begin with a bang, `!`

## Alternatives

Expand Down
95 changes: 32 additions & 63 deletions src/context.jl
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ function lookup(ctx::Context, key)
if occursin(r"\.", key)

value′ = lookup_dotted(context, key)

if value′ == nothing
## do something with "."
## we use .[ind] to refer to value in parent of given index;
Expand Down Expand Up @@ -100,59 +101,38 @@ end

## Lookup value in an object by key
## This of course varies based on the view.
## we special case dataframes here, so that we don't have to assume package is loaded
## After checking several specific types, if view is Tables compatible
## this will return "column" corresponding to the key
function lookup_in_view(view, key)
if Tables.istable(view)
if isempty(Tables.rows(view))
return nothing
else
rows = Tables.rows(view)
sch = Tables.schema(rows)
if sch == nothing
## schema is unknown or non inferrable
## What to do?
r = getfield(first(rows), 1)
k = occursin(r"^:", key) ? Symbol(key[2:end]) : key
if isa(r, Pair)
return k == r.first ? r.second : nothing
else
return k in propertynames(r) ? getproperty(r, k) : nothing
end
else
# work with a dictionary from the IteratorRow interface
# follows "Sinks (transferring data from one table to another)"
out = Any[]
for row in rows
rD = Dict()
Tables.eachcolumn(sch, row) do val, col, name
rD[name] = val
end
k = occursin(r"^:", key) ? Symbol(key[2:end]) : key
push!(out, get(rD,k, nothing))
end
return out
end
end
elseif is_dataframe(view)

if occursin(r"^:", key) key = key[2:end] end
key = Symbol(key)
out = nothing
if haskey(view, key)
out = view[1, key] ## first element only
val = _lookup_in_view(view, key)
!falsy(val) && return val

if Tables.istable(view)
isempty(Tables.rows(view)) && return nothing
sch = Tables.schema(Tables.rows(view))
falsy(sch) && return nothing
k = normalize(key)
if k sch.names
return [row[k] for row Tables.rows(view)]
end
out
# elseif is_dataframe(view)

# if occursin(r"^:", key) key = key[2:end] end
# key = Symbol(key)
# out = nothing
# if haskey(view, key)
# out = view[1, key] ## first element only
# end
# out
else
_lookup_in_view(view, key)
__lookup_in_view(view, key)
end
end


# look up key in view, return `nothing` if not found
function _lookup_in_view(view::AbstractDict, key)
## is it a symbol?
k = startswith(key, ":") ? Symbol(key[2:end]) : key
get(view, k, nothing)

get(view, normalize(key), nothing)
end

# support legacy use of `first` and `second` as variable names
Expand All @@ -161,20 +141,11 @@ function _lookup_in_view(view::Pair, key)
## is it a symbol?
key == "first" && return view.first
key == "second" && return view.second
k = startswith(key, ":") ? Symbol(key[2:end]) : key
view.first == k ? view.second : nothing
view.first == normalize(key) ? view.second : nothing
end

function _lookup_in_view(view::NamedTuple, key)
## is it a symbol?
if occursin(r"^:", key)
key = Symbol(key[2:end])
end
if haskey(view, key)
getindex(view, key)
else
nothing
end
get(view, normalize(key), nothing)
end

function _lookup_in_view(view::Module, key)
Expand All @@ -196,14 +167,12 @@ function _lookup_in_view(view::Module, key)

end

## Default is likely not great,
function _lookup_in_view(view, key)
_lookup_in_view(view, key) = nothing

if occursin(r"^:", key)
k = Symbol(key[2:end])
else
k = key
end
## Default lookup is likely not great,
function __lookup_in_view(view, key)

k = normalize(key)

# check propertyname, then fieldnames
if k in propertynames(view)
Expand Down
12 changes: 5 additions & 7 deletions src/render.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ function render(io::IO, tokens::MustacheTokens, view)
render(io, _writer, tokens, view)
end
function render(io::IO, tokens::MustacheTokens; kwargs...)
render(io, tokens, kwargs)
render(io, tokens, Dict(kwargs...))
end

render(tokens::MustacheTokens, view) = sprint(io -> render(io, tokens, view))
render(tokens::MustacheTokens; kwargs...) = sprint(io -> render(io, tokens, Dict(kwargs...)))
render(tokens::MustacheTokens; kwargs...) = sprint(io -> render(io, tokens; kwargs...))

## make MustacheTokens callable for kwargs...
function (m::MustacheTokens)(io::IO, args...; kwargs...)
Expand All @@ -46,15 +46,13 @@ end
## @param template a string containing the template for expansion
## @param view a Dict, Module, CompositeType, DataFrame holding variables for expansion
function render(io::IO, template::AbstractString, view; tags= ("{{", "}}"))
_writer = Writer()
render(io, _writer, parse(template, tags), view)
return render(io, parse(template, tags), view)
end
function render(io::IO, template::AbstractString; kwargs...)
_writer = Writer()
render(io, _writer, parse(template), Dict(kwargs...))
return render(io, parse(template); kwargs...)
end
render(template::AbstractString, view; tags=("{{", "}}")) = sprint(io -> render(io, template, view, tags=tags))
render(template::AbstractString; kwargs...) = sprint(io -> render(io, template, Dict(kwargs...)))
render(template::AbstractString; kwargs...) = sprint(io -> render(io, template; kwargs...))


# Exported, but should be deprecated....
Expand Down
71 changes: 15 additions & 56 deletions src/tokens.jl
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ function make_tokens(template, tags)
push!(tokens, tag_token)

# account for matching/nested sections
if _type == "#" || _type == "^" || _type == "|" || _type == "@"
if _type == "#" || _type == "^" || _type == "|" || _type == "@"
push!(sections, tag_token)
elseif _type == "/"
## section nestinng
Expand Down Expand Up @@ -385,7 +385,7 @@ function nestTokens(tokens)
## a {{#name}}...{{/name}} will iterate over name
## a {{^name}}...{{/name}} does ... if we have no name
## start nesting
if token._type == "^" || token._type == "#" || token._type == "|" || token._type == "@"
if token._type == "^" || token._type == "#" || token._type == "|" || token._type == "@"
push!(sections, token)
push!(collector, token)
token.collector = Array{Any}(undef, 0)
Expand Down Expand Up @@ -437,35 +437,18 @@ function _toString(::Val{Symbol("#")}, t)
end



## ----------------------------------------------------

# render tokens with values given in context
function renderTokensByValue(value, io, token, writer, context, template, args...)
inverted = token._type == "^"
if (inverted && falsy(value))
_renderTokensByValue(value, io, token, writer, context, template, args...)
elseif Tables.istable(value)
if isempty(Tables.rows(value))
return nothing
else
rows = Tables.rows(value)
sch = Tables.schema(rows)
rD = Dict()
for row in rows
if sch == nothing
_renderTokensByValue(getfield(row,1), io, token, writer, context, template, args...)
else
# create a dictionary, though perhaps getfield is general enough
Tables.eachcolumn(sch, row) do val, col, name
rD[name] = val
end
renderTokens(io, token.collector, writer, ctx_push(context, rD), template, args...)
end
end
end
elseif is_dataframe(value) # XXX remove once istable(x::DataFrame) == true works
for i in 1:size(value)[1]
renderTokens(io, token.collector, writer, ctx_push(context, value[i,:]), template, args...)
isempty(Tables.rows(value)) && return nothing
rows = Tables.rows(value)
for row in rows
renderTokens(io, token.collector, writer, ctx_push(context, row), template, args...)
end
elseif !falsy(value)
_renderTokensByValue(value, io, token, writer, context, template, args...)
Expand All @@ -475,35 +458,12 @@ function renderTokensByValue(value, io, token, writer, context, template, args..
end
end

# function renderTokensByValue(value, io, token, writer, context, template, args...)
# if Tables.istable(value)
# if isempty(Tables.rows(value))
# @show "no length", token._type

# return nothing
# else
# for row in Tables.rows(value)
# _renderTokensByValue(getfield(row,1), io, token, writer, context, template, args...)
# end
# end
# elseif is_dataframe(value) # XXX remove once istable(x::DataFrame) == true works
# for i in 1:size(value)[1]
# renderTokens(io, token.collector, writer, ctx_push(context, value[i,:]), template, args...)
# end
# else
# inverted = token._type == "^"
# if (inverted && falsy(value)) || !falsy(value)
# _renderTokensByValue(value, io, token, writer, context, template, args...)
# end
# end
# end

## Helper function for dispatch based on value in renderTokens
function _renderTokensByValue(value::AbstractDict, io, token, writer, context, template, args...)
renderTokens(io, token.collector, writer, ctx_push(context, value), template, args...)
renderTokens(io, token.collector, writer, ctx_push(context, value), template, args...)
end

function _renderTokensByValue(value::Union{AbstractArray, Tuple}, io, token, writer, context, template, args...)
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...)
Expand All @@ -515,13 +475,6 @@ function _renderTokensByValue(value::Union{AbstractArray, Tuple}, io, token, wri
end
end

## ## DataFrames
## function renderTokensByValue(value::DataFrames.DataFrame, io, token, writer, context, template)
## ## iterate along row, Call one for each row
## for i in 1:size(value)[1]
## renderTokens(io, token.collector, writer, ctx_push(context, value[i,:]), template)
## end
## 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
Expand Down Expand Up @@ -590,13 +543,17 @@ 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 @@ -641,7 +598,9 @@ function renderTokens(io, tokens, writer, context, template, idx=(0,0))
end
tpl_string = String(take!(buf))
else

value = lookup(context, fname)

if !falsy(value)
indent = token.indent
slashn = ""
Expand Down
11 changes: 10 additions & 1 deletion src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ end
## this is for falsy value
## Falsy is true if x is false, 0 length, "", ...
falsy(x::Bool) = !x
falsy(x::Array) = isempty(x)
falsy(x::Array) = isempty(x) || all(falsy, x)
falsy(x::AbstractString) = x == ""
falsy(x::Nothing) = true
falsy(x::Missing) = true
Expand Down Expand Up @@ -76,6 +76,15 @@ function escapeTags(tags)
Regex("\\s*" * escapeRe(tags[2]))]
end

# key may be string or a ":symbol"
function normalize(key)
if occursin(r"^:", key)
key = key[2:end]
key = Symbol(key)
end
return key
end


## hueristic to avoid loading DataFrames
## Once `Tables.jl` support for DataFrames is available, this can be dropped
Expand Down
Loading

2 comments on commit 94fc3f7

@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/48452

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.12 -m "<description of version>" 94fc3f760950df28dc51dcbeba1b95396a08d8b0
git push origin v1.0.12

Please sign in to comment.