Repository: tlienart/Franklin.jl
Pull request #416 (2020-03-19 10:33:57)

Html tokens
Beyond the alias already introduced this allows the following:

in markdown
```
@def iter = ("a"=>10, "b"=>7, "c"=>3)
```

in html
```
ABC
{{for (n, v) in iter2}}
  name:{{fill n}}
  value:{{fill v}}
{{end}}
```

There can be arbitrarily many variables to unpack, not just 2 like here.


Patch:
diff --git a/Project.toml b/Project.toml
index 56d868e3..0addacd0 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,7 +1,7 @@
 name = "Franklin"
 uuid = "713c75ef-9fc9-4b05-94a9-213340da978e"
 authors = ["Thibaut Lienart <tlienart@me.com>"]
-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 d160add5..81690db0 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 2d903ac2..a7937e8d 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 83401b89..443b762c 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 > (&#33;)? == either ! or nothing
-    # 2 > &#91;(.*?)&#93; == [...] inside of the brackets
-    # 3 > (?:&#91;(.*?)&#93;)? == [...] inside of second brackets if there is such
-    rx = r"(&#33;)?&#91;(.*?)&#93;(?!:)(?:&#91;(.*?)&#93;)?"
-    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 3b2575d9..1385deb3 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 980090fd..06a16354 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 00000000..6ba9b9f8
--- /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 > (&#33;)? == either ! or nothing
+# 2 > &#91;(.*?)&#93; == [...] inside of the brackets
+# 3 > (?:&#91;(.*?)&#93;)? == [...] inside of second brackets if there is such
+const ESC_LINK_PAT = r"(&#33;)?&#91;(.*?)&#93;(?!:)(?:&#91;(.*?)&#93;)?"
+
+#= =====================================================
+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/src/utils/vars.jl b/src/utils/vars.jl
index 6804c50a..bbb8d85f 100644
--- a/src/utils/vars.jl
+++ b/src/utils/vars.jl
@@ -111,11 +111,17 @@ function def_LOCAL_VARS!()::Nothing
     return nothing
 end
 
-"""Convenience function to get the value associated with a local var."""
-locvar(name::String) = LOCAL_VARS[name].first
+"""
+Convenience function to get the value associated with a local var.
+Return `nothing` if the variable is not found.
+"""
+locvar(name::String) = haskey(LOCAL_VARS, name) ? LOCAL_VARS[name].first : nothing
 
-"""Convenience function to get the value associated with a global var."""
-globvar(name::String) = GLOBAL_VARS[name].first
+"""
+Convenience function to get the value associated with a global var.
+Return `nothing` if the variable is not found.
+"""
+globvar(name::String) = haskey(GLOBAL_VARS, name) ? GLOBAL_VARS[name].first : nothing
 
 """
 Keep track of seen headers. The key is the refstring, the value contains the


Test patch:
diff --git a/test/converter/html_for.jl b/test/converter/html_for.jl
index 9f4d036e..2a086817 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/converter/md_defs.jl b/test/converter/md_defs.jl
index fd4bf75b..7da7d247 100644
--- a/test/converter/md_defs.jl
+++ b/test/converter/md_defs.jl
@@ -87,4 +87,6 @@ end
         """ |> fd2html_td
     @test isapproxstr(F.locvar("s"), "foo bar baz etc")
     @test isapproxstr(F.locvar("s2"), "nothing")
+    @test F.locvar("foobar") === nothing
+    @test F.globvar("foobar") === nothing
 end
diff --git a/test/regexes.jl b/test/regexes.jl
new file mode 100644
index 00000000..fedaa87d
--- /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 46efd220..fa9f88b1 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 74be6a74..49174e53 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