From 94fc3f760950df28dc51dcbeba1b95396a08d8b0 Mon Sep 17 00:00:00 2001 From: john verzani Date: Tue, 9 Nov 2021 06:47:58 -0500 Subject: [PATCH] close issue #139 (#140) --- Project.toml | 4 +- docs/src/index.md | 5 ++- src/context.jl | 95 +++++++++++++++---------------------------- src/render.jl | 12 +++--- src/tokens.jl | 71 +++++++------------------------- src/utils.jl | 11 ++++- test/Mustache_test.jl | 64 +++++++++++++++++++++-------- 7 files changed, 115 insertions(+), 147 deletions(-) diff --git a/Project.toml b/Project.toml index 48b209b..d3aa0e2 100644 --- a/Project.toml +++ b/Project.toml @@ -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] diff --git a/docs/src/index.md b/docs/src/index.md index e4f9649..166d028 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -469,7 +469,8 @@ 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}}`. @@ -477,7 +478,7 @@ To summarize the different tags marking a variable: * `{{^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`). * `{{ 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...) @@ -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.... diff --git a/src/tokens.jl b/src/tokens.jl index 3806225..2a27528 100644 --- a/src/tokens.jl +++ b/src/tokens.jl @@ -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 @@ -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) @@ -437,7 +437,7 @@ function _toString(::Val{Symbol("#")}, t) end - +## ---------------------------------------------------- # render tokens with values given in context function renderTokensByValue(value, io, token, writer, context, template, args...) @@ -445,27 +445,10 @@ function renderTokensByValue(value, io, token, writer, context, template, args.. 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...) @@ -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...) @@ -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 @@ -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) @@ -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 = "" diff --git a/src/utils.jl b/src/utils.jl index 3e56241..ba9a790 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -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 @@ -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 diff --git a/test/Mustache_test.jl b/test/Mustache_test.jl index 244f0ae..aa5bf80 100644 --- a/test/Mustache_test.jl +++ b/test/Mustache_test.jl @@ -215,34 +215,66 @@ tpl2 = mt""" @test render(tpl, Dict("dims"=>1:2)) == "\n\n" ## issue 128 global versus local -d = Dict(:two=>Dict(:x=>3), :x=>2) -tpl = mt""" + d = Dict(:two=>Dict(:x=>3), :x=>2) + tpl = mt""" {{#:one}} {{#:two}} {{:x}} {{/:two}} {{/:one}} """ -@test render(tpl, one=d) == "3\n" + @test render(tpl, one=d) == "3\n" -tpl = mt""" + tpl = mt""" {{#:one}} {{#:two}} {{~:x}} {{/:two}} {{/:one}} """ -@test render(tpl, one=d) == "2\n" -@test render(tpl, one=d, x=1) == "1\n" - -## Issue #133 triple brace with } -tpl = raw"\includegraphics{<<{:filename}>>}" -tokens = Mustache.parse(tpl, ("<<",">>")) -@test render(tokens, filename="XXX") == raw"\includegraphics{XXX}" - -## jmt macro -x = 1 -tpl = jmt"$(2x) by {{:a}}" -@test tpl(a=2) == "2 by 2" + @test render(tpl, one=d) == "2\n" + @test render(tpl, one=d, x=1) == "1\n" + + ## Issue #133 triple brace with } + tpl = raw"\includegraphics{<<{:filename}>>}" + tokens = Mustache.parse(tpl, ("<<",">>")) + @test render(tokens, filename="XXX") == raw"\includegraphics{XXX}" + + # alternative is to use `&` to avoid escaping + @test render(raw"\includegraphics{<<&:filename>>}", (filename="XXX",), #render(string, view;tags=...) + tags=("<<",">>")) == raw"\includegraphics{XXX}" + + + ## jmt macro + x = 1 + tpl = jmt"$(2x) by {{:a}}" + @test tpl(a=2) == "2 by 2" + + + ## Issue #139 -- mishandling of tables data with partials + A = [Dict("a" => "eh", "b" => "bee"), + Dict("a" => "ah", "b" => "buh")] + tpl = mt"{{#:A}}Pronounce a as {{>:d}} and b as {{b}}. {{/:A}}" + out1 = render(tpl, A=A, d="*{{a}}*") + + A = [Dict(:a => "eh", :b => "bee"), + Dict(:a => "ah", :b => "buh")] + tpl = mt"{{#:A}}Pronounce a as {{>:d}} and b as {{:b}}. {{/:A}}" + out2 = render(tpl, A=A, d="*{{:a}}*") + + A = [(a = "eh", b = "bee"), + (a = "ah", b = "buh")] + tpl = mt"{{#:A}}Pronounce a as {{>:d}} and b as {{:b}}. {{/:A}}" + out3 = render(tpl, A=A, d="*{{:a}}*") + @test out1 == out2 == out3 + + ## lookup in Tables compatible data + ## find column + tpl = mt"{{#:vec}}{{.}} {{/:vec}}" + A = [(vec=1, a=2), + (vec=2, a=3), + (vec=3, a=4)] + @test render(tpl, A) == "1 2 3 " + end