diff --git a/Project.toml b/Project.toml index 56d868e3c..0addacd04 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Franklin" uuid = "713c75ef-9fc9-4b05-94a9-213340da978e" authors = ["Thibaut Lienart "] -version = "0.6.8" +version = "0.6.9" [deps] Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" diff --git a/src/Franklin.jl b/src/Franklin.jl index d160add5d..81690db02 100644 --- a/src/Franklin.jl +++ b/src/Franklin.jl @@ -105,6 +105,8 @@ const MESSAGE_FILE_GEN_FMD = "# $MESSAGE_FILE_GEN # hide\n" include("build.jl") # check if user has Node/minify +include("regexes.jl") + # UTILS include("utils/paths.jl") include("utils/vars.jl") diff --git a/src/converter/html/blocks.jl b/src/converter/html/blocks.jl index 2d903ac2d..a7937e8dd 100644 --- a/src/converter/html/blocks.jl +++ b/src/converter/html/blocks.jl @@ -167,11 +167,10 @@ function process_html_for(hs::AS, qblocks::Vector{AbstractBlock}, iname = β_open.iname if !haskey(LOCAL_VARS, iname) throw(HTMLBlockError("The iterable '$iname' is not recognised. " * - "Make sure it's defined.")) + "Please make sure it's defined.")) end # try to close the for loop - if i == length(qblocks) throw(HTMLBlockError("Could not close the conditional block " * "starting with '$(qblocks[i].ss)'.")) @@ -197,17 +196,48 @@ function process_html_for(hs::AS, qblocks::Vector{AbstractBlock}, β_close = qblocks[i] i_close = i + isempty(locvar(iname)) && @goto final_step + + # is vname a single variable or multiple variables? + # --> {{for v in iterate}} + # --> {{for (v1, v2) in iterate }} + vnames = [vname] + if startswith(vname, "(") + vnames = strip.(split(vname[2:end-1], ",")) + end + # check that the first element of the iterate has the same length + el1 = first(locvar(iname)) + length(vnames) in (1, length(el1)) || + throw(HTMLBlockError("In a {{for ...}}, the first element of" * + "the iterate has length $(length(el1)) but tried to unpack" * + "it as $(length(vnames)) variables.")) + # so now basically we have to simply copy-paste the content replacing # variable `vname` when it appears in a html block {{...}} # users should try not to be dumb about this... if vname or iname # corresponds to something they shouldn't touch, they'll crash things. + + # content of the for block inner = subs(hs, nextind(hs, to(β_open)), prevind(hs, from(β_close))) + isempty(strip(inner)) && @goto final_step content = "" - for value in locvar(iname) - # at the moment we only consider {{fill ...}} - content *= replace(inner, - Regex("({{\\s*fill\\s+$vname\\s*}})") => "$value") + if length(vnames) == 1 + for value in locvar(iname) + # at the moment we only consider {{fill ...}} + content *= replace(inner, + Regex("({{\\s*fill\\s+$vname\\s*}})") => "$value") + end + else + for value in locvar(iname) + temp = inner + for (vname, value) in zip(vnames, value) + temp = replace(temp, + Regex("({{\\s*fill\\s+$vname\\s*}})") => "$value") + end + content *= temp + end end + @label final_step head = nextind(hs, to(β_close)) return convert_html(content), head, i_close end diff --git a/src/converter/html/link_fixer.jl b/src/converter/html/link_fixer.jl index 83401b895..443b762cd 100644 --- a/src/converter/html/link_fixer.jl +++ b/src/converter/html/link_fixer.jl @@ -9,13 +9,7 @@ Direct inline-style links are properly processed by Julia's Markdown processor b """ function find_and_fix_md_links(hs::String)::String # 1. find all occurences of -- [...]: link - - # here we're looking for [id] or [id][] or [stuff][id] or ![stuff][id] but not [id]: - # 1 > (!)? == either ! or nothing - # 2 > [(.*?)] == [...] inside of the brackets - # 3 > (?:[(.*?)])? == [...] inside of second brackets if there is such - rx = r"(!)?[(.*?)](?!:)(?:[(.*?)])?" - m_link_refs = collect(eachmatch(rx, hs)) + m_link_refs = collect(eachmatch(ESC_LINK_PAT, hs)) # recuperate the appropriate id which has a chance to match def_names ref_names = [ diff --git a/src/parser/html/blocks.jl b/src/parser/html/blocks.jl index 3b2575d9b..1385deb30 100644 --- a/src/parser/html/blocks.jl +++ b/src/parser/html/blocks.jl @@ -39,7 +39,12 @@ function qualify_html_hblocks(blocks::Vector{OCBlock})::Vector{AbstractBlock} # --- # for block m = match(HBLOCK_FOR_PAT, β.ss) - isnothing(m) || (qb[i] = HFor(β.ss, m.captures[1], m.captures[2]); continue) + if !isnothing(m) + v, iter = m.captures + check_for_pat(v) + qb[i] = HFor(β.ss, v, iter); + continue + end # --- # function block {{ fname v1 v2 ... }} m = match(HBLOCK_FUN_PAT, β.ss) diff --git a/src/parser/html/tokens.jl b/src/parser/html/tokens.jl index 980090fd0..06a16354a 100644 --- a/src/parser/html/tokens.jl +++ b/src/parser/html/tokens.jl @@ -48,29 +48,6 @@ at this point. This second point might fix the first one by making sure that HElse -> HEnd =# -""" -HBLOCK_IF_PAT # {{if v1}} -HBLOCK_ELSE_PAT # {{else}} -HBLOCK_ELSEIF_PAT # {{elseif v1}} -HBLOCK_END_PAT # {{end}} -HBLOCK_ISDEF_PAT # {{isdef v1}} alias: ifdef -HBLOCK_ISNOTDEF_PAT # {{isnotdef v1}} alias: ifnotdef,isndef,ifndef -HBLOCK_ISPAGE_PAT # {{ispage p1 p2}} -HBLOCK_ISNOTPAGE_PAT # {{isnotpage p1 p2}} -HBLOCK_FOR_PAT # {{for x in iterable}} - -Regex for the different HTML tokens. -""" -const HBLOCK_IF_PAT = r"{{\s*if\s+([a-zA-Z_]\S*)\s*}}" -const HBLOCK_ELSE_PAT = r"{{\s*else\s*}}" -const HBLOCK_ELSEIF_PAT = r"{{\s*else\s*if\s+([a-zA-Z_]\S*)\s*}}" -const HBLOCK_END_PAT = r"{{\s*end\s*}}" -const HBLOCK_ISDEF_PAT = r"{{\s*i(?:s|f)def\s+([a-zA-Z_]\S*)\s*}}" -const HBLOCK_ISNOTDEF_PAT = r"{{\s*i(?:s|f)n(?:ot)?def\s+([a-zA-Z_]\S*)\s*}}" -const HBLOCK_ISPAGE_PAT = r"{{\s*ispage\s+((.|\n)+?)}}" -const HBLOCK_ISNOTPAGE_PAT = r"{{\s*isnotpage\s+((.|\n)+?)}}" -const HBLOCK_FOR_PAT = r"{{\s*for\s+([a-zA-Z_]\S*)\s+in\s+([a-zA-Z_]\S*)\s*}}" - """ $(TYPEDEF) @@ -217,14 +194,6 @@ struct HFun <: AbstractBlock end -""" -HBLOCK_TOC_PAT - -Insertion for a table of contents. -""" -const HBLOCK_TOC_PAT = r"{{\s*toc\s*}}" - - """ $(TYPEDEF) diff --git a/src/regexes.jl b/src/regexes.jl new file mode 100644 index 000000000..6ba9b9f87 --- /dev/null +++ b/src/regexes.jl @@ -0,0 +1,47 @@ +#= ===================================================== +LINK patterns, see html link fixer +===================================================== =# +# here we're looking for [id] or [id][] or [stuff][id] or ![stuff][id] but not [id]: +# 1 > (!)? == either ! or nothing +# 2 > [(.*?)] == [...] inside of the brackets +# 3 > (?:[(.*?)])? == [...] inside of second brackets if there is such +const ESC_LINK_PAT = r"(!)?[(.*?)](?!:)(?:[(.*?)])?" + +#= ===================================================== +HBLOCK patterns, see html blocks +NOTE: the for block needs verification (matching parens) +===================================================== =# + +const HBLOCK_IF_PAT = r"{{\s*if\s+([a-zA-Z_]\S*)\s*}}" +const HBLOCK_ELSE_PAT = r"{{\s*else\s*}}" +const HBLOCK_ELSEIF_PAT = r"{{\s*else\s*if\s+([a-zA-Z_]\S*)\s*}}" +const HBLOCK_END_PAT = r"{{\s*end\s*}}" + +const HBLOCK_ISDEF_PAT = r"{{\s*i(?:s|f)def\s+([a-zA-Z_]\S*)\s*}}" +const HBLOCK_ISNOTDEF_PAT = r"{{\s*i(?:s|f)n(?:ot)?def\s+([a-zA-Z_]\S*)\s*}}" +const HBLOCK_ISPAGE_PAT = r"{{\s*ispage\s+((.|\n)+?)}}" +const HBLOCK_ISNOTPAGE_PAT = r"{{\s*isnotpage\s+((.|\n)+?)}}" + +const HBLOCK_FOR_PAT = r"{{\s*for\s+(\(?(?:\s*[a-zA-Z_][^\r\n\t\f\v,]*,\s*)*[a-zA-Z_]\S*\s*\)?)\s+in\s+([a-zA-Z_]\S*)\s*}}" + +const HBLOCK_TOC_PAT = r"{{\s*toc\s*}}" + +#= ===================================================== +Pattern checkers +===================================================== =# + +""" + check_for_pat(v) + +Check that we have something like `{{for v in iterate}}` or +`{for (v1,v2) in iterate}}` but not something with unmached parens. +""" +function check_for_pat(v) + op = startswith(v, "(") + cp = endswith(v, ")") + xor(op, cp) && + throw(HTMLBlockError("Unbalanced expression in {{for ...}}")) + !op && occursin(",", v) && + throw(HTMLBlockError("Missing parens in {{for ...}}")) + return nothing +end diff --git a/test/converter/html_for.jl b/test/converter/html_for.jl index 9f4d036e2..2a0868173 100644 --- a/test/converter/html_for.jl +++ b/test/converter/html_for.jl @@ -39,3 +39,46 @@ end path/to/badge2.png """) end + +@testset "h-for3" begin + F.def_LOCAL_VARS!() + s = """ + @def iter = (("a", 1), ("b", 2), ("c", 3)) + """ |> fd2html_td + h = raw""" + ABC + {{for (n, v) in iter}} + name:{{fill n}} + value:{{fill v}} + {{end}} + """ |> F.convert_html + @test isapproxstr(h, """ + ABC + name:a + value:1 + name:b + value:2 + name:c + value:3 + """) + + s = """ + @def iter2 = ("a"=>10, "b"=>7, "c"=>3) + """ |> fd2html_td + h = raw""" + ABC + {{for (n, v) in iter2}} + name:{{fill n}} + value:{{fill v}} + {{end}} + """ |> F.convert_html + @test isapproxstr(h, """ + ABC + name:a + value:10 + name:b + value:7 + name:c + value:3 + """) +end diff --git a/test/regexes.jl b/test/regexes.jl new file mode 100644 index 000000000..fedaa87db --- /dev/null +++ b/test/regexes.jl @@ -0,0 +1,191 @@ +### ESC LINK +@testset "esclink" begin + s = Markdown.htmlesc("[hello]") + a,b,c = match(F.ESC_LINK_PAT, s).captures + @test isnothing(a) + @test b == "hello" + @test isnothing(c) + s = Markdown.htmlesc("[hello][]") + a,b,c = match(F.ESC_LINK_PAT, s).captures + @test isnothing(a) + @test b == "hello" + @test isempty(c) + s = Markdown.htmlesc("[hello][id]") + a,b,c = match(F.ESC_LINK_PAT, s).captures + @test isnothing(a) + @test b == "hello" + @test c == "id" + s = Markdown.htmlesc("![hello][id]") + a,b,c = match(F.ESC_LINK_PAT, s).captures + @test a == Markdown.htmlesc("!") + @test b == "hello" + @test c == "id" + s = Markdown.htmlesc("[hello]:") + @test isnothing(match(F.ESC_LINK_PAT, s)) +end + +### HBLOCK REGEXES + +@testset "hb-if" begin + for s in ( + "{{if var1}}", + "{{if var1 }}", + "{{ if var1 }}", + ) + m = match(F.HBLOCK_IF_PAT, s) + @test m.captures[1] == "var1" + end + for s in ( + "{{if var1 var2}}", + "{{ifvar1}}" + ) + m = match(F.HBLOCK_IF_PAT, s) + @test isnothing(m) + end +end +@testset "hb-else" begin + for s in ( + "{{else}}", + "{{ else}}", + "{{ else }}", + ) + m = match(F.HBLOCK_ELSE_PAT, s) + @test !isnothing(m) + end +end +@testset "hb-elseif" begin + for s in ( + "{{elseif var1}}", + "{{else if var1 }}", + "{{ elseif var1 }}", + ) + m = match(F.HBLOCK_ELSEIF_PAT, s) + @test m.captures[1] == "var1" + end + for s in ( + "{{else if var1 var2}}", + "{{elif var1}}" + ) + m = match(F.HBLOCK_ELSEIF_PAT, s) + @test isnothing(m) + end +end +@testset "hb-end" begin + for s in ( + "{{end}}", + "{{ end}}", + "{{ end }}", + ) + m = match(F.HBLOCK_END_PAT, s) + @test !isnothing(m) + end +end +@testset "hb-isdef" begin + for s in ( + "{{isdef var1}}", + "{{ isdef var1 }}", + "{{ isdef var1 }}", + "{{ ifdef var1 }}", + ) + m = match(F.HBLOCK_ISDEF_PAT, s) + @test m.captures[1] == "var1" + end + for s in ( + "{{isdef var1 var2}}", + "{{is def var1}}", + "{{if def var1}}" + ) + m = match(F.HBLOCK_ISDEF_PAT, s) + @test isnothing(m) + end +end +@testset "hb-isndef" begin + for s in ( + "{{isnotdef var1}}", + "{{ isndef var1 }}", + "{{ ifndef var1 }}", + "{{ ifnotdef var1 }}", + ) + m = match(F.HBLOCK_ISNOTDEF_PAT, s) + @test m.captures[1] == "var1" + end + for s in ( + "{{isnotdef var1 var2}}", + "{{isnot def var1}}", + "{{ifn def var1}}" + ) + m = match(F.HBLOCK_ISNOTDEF_PAT, s) + @test isnothing(m) + end +end +@testset "hb-ispage" begin + for s in ( + "{{ispage var1 var2}}", + ) + m = match(F.HBLOCK_ISPAGE_PAT, s) + @test m.captures[1] == "var1 var2" + end +end +@testset "hb-isnotpage" begin + for s in ( + "{{isnotpage var1 var2}}", + ) + m = match(F.HBLOCK_ISNOTPAGE_PAT, s) + @test m.captures[1] == "var1 var2" + end +end +@testset "hb-for" begin + for s in ( + "{{for (v1,v2,v3) in iterate}}", + "{{for (v1, v2,v3) in iterate}}", + "{{for ( v1, v2, v3) in iterate}}", + "{{for ( v1 , v2 , v3 ) in iterate}}" + ) + m = match(F.HBLOCK_FOR_PAT, s) + @test isapproxstr(m.captures[1], "(v1, v2, v3)") + end + s = "{{for v1 in iterate}}" + m = match(F.HBLOCK_FOR_PAT, s) + @test isapproxstr(m.captures[1], "v1") + + # WARNING: NOT RECOMMENDED / NEEDS CARE + s = "{{for v1,v2 in iterate}}" + m = match(F.HBLOCK_FOR_PAT, s) + @test isapproxstr(m.captures[1], "v1,v2") + s = "{{for (v1,v2 in iterate}}" + m = match(F.HBLOCK_FOR_PAT, s) + @test isapproxstr(m.captures[1], "(v1,v2") + s = "{{for v1,v2) in iterate}}" + m = match(F.HBLOCK_FOR_PAT, s) + @test isapproxstr(m.captures[1], "v1,v2)") +end +@testset "hb-toc" begin + for s in ( + "{{toc}}", + "{{ toc }}" + ) + m = match(F.HBLOCK_TOC_PAT, s) + @test !isnothing(m) + end +end + +# ======== +# Checkers +# ======== +@testset "ch-for" begin + s = "{{for v in iterate}}" + m = match(F.HBLOCK_FOR_PAT, s).captures[1] + @test isnothing(F.check_for_pat(m)) + s = "{{for (v1,v2) in iterate}}" + m = match(F.HBLOCK_FOR_PAT, s).captures[1] + @test isnothing(F.check_for_pat(m)) + s = "{{for (v in iterate}}" + m = match(F.HBLOCK_FOR_PAT, s).captures[1] + @test_throws F.HTMLBlockError F.check_for_pat(m) + s = "{{for v1,v2) in iterate}}" + m = match(F.HBLOCK_FOR_PAT, s).captures[1] + @test_throws F.HTMLBlockError F.check_for_pat(m) + s = "{{for v1,v2 in iterate}}" + m = match(F.HBLOCK_FOR_PAT, s).captures[1] + @test_throws F.HTMLBlockError F.check_for_pat(m) +end diff --git a/test/runtests.jl b/test/runtests.jl index 46efd2203..fa9f88b14 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -13,6 +13,7 @@ include("utils/paths_vars.jl"); include("test_utils.jl") include("utils/misc.jl") include("utils/errors.jl") include("utils/html.jl") +include("regexes.jl") println("🍺") # MANAGER folder diff --git a/test/test_utils.jl b/test/test_utils.jl index 74be6a74c..49174e532 100644 --- a/test/test_utils.jl +++ b/test/test_utils.jl @@ -14,8 +14,8 @@ set_curpath(path) = # convenience function that squeezes out all whitespaces and line returns out of a string # and checks if the resulting strings are equal. When expecting a specific string +- some # spaces, this is very convenient. Use == if want to check exact strings. -isapproxstr(s1::String, s2::String) = - isequal(map(s->replace(s, r"\s|\n"=>""), (s1, s2))...) +isapproxstr(s1::AbstractString, s2::AbstractString) = + isequal(map(s->replace(s, r"\s|\n"=>""), String.((s1, s2)))...) # this is a slightly ridiculous requirement but apparently the `eval` blocks # don't play well with Travis nor windows while testing, so you just need to forcibly