Repository: tlienart/Franklin.jl
Pull request #370 (2020-02-20 19:16:47)

Allow multi-line defs and for loops
Now you can do

```
@def list = begin
    x = [5, 2, 3]
    y = x .+ 1
    end
Then
~~~
{{for x in list}}
    Hello: {{fill x}}
{{end}}
~~~
Done
```

and it would essentially lead to

```html
<p>Then 
Hello: 6
Hello: 3
Hello: 4
Done</p>
```

**Note**: template for loops are still rudimentary, they're only meant to be used via `{{fill ...}}`

codecov[bot]: # [Codecov](https://codecov.io/gh/tlienart/Franklin.jl/pull/370?src=pr&el=h1) Report
> Merging [#370](https://codecov.io/gh/tlienart/Franklin.jl/pull/370?src=pr&el=desc) into [master](https://codecov.io/gh/tlienart/Franklin.jl/commit/6357d3d04baf06effed795db386860a7fd902d8c?src=pr&el=desc) will **increase** coverage by `0.22%`.
> The diff coverage is `97.1%`.

[![Impacted file tree graph](https://codecov.io/gh/tlienart/Franklin.jl/pull/370/graphs/tree.svg?width=650&token=6aP21CmJcY&height=150&src=pr)](https://codecov.io/gh/tlienart/Franklin.jl/pull/370?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##           master    #370      +/-   ##
=========================================
+ Coverage   87.48%   87.7%   +0.22%     
=========================================
  Files          38      39       +1     
  Lines        1933    1993      +60     
=========================================
+ Hits         1691    1748      +57     
- Misses        242     245       +3
```


| [Impacted Files](https://codecov.io/gh/tlienart/Franklin.jl/pull/370?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/parser/markdown/tokens.jl](https://codecov.io/gh/tlienart/Franklin.jl/pull/370/diff?src=pr&el=tree#diff-c3JjL3BhcnNlci9tYXJrZG93bi90b2tlbnMuamw=) | `100% <ø> (ø)` | :arrow_up: |
| [src/Franklin.jl](https://codecov.io/gh/tlienart/Franklin.jl/pull/370/diff?src=pr&el=tree#diff-c3JjL0ZyYW5rbGluLmps) | `100% <ø> (ø)` | :arrow_up: |
| [src/utils/vars.jl](https://codecov.io/gh/tlienart/Franklin.jl/pull/370/diff?src=pr&el=tree#diff-c3JjL3V0aWxzL3ZhcnMuamw=) | `100% <100%> (ø)` | :arrow_up: |
| [src/converter/markdown/md.jl](https://codecov.io/gh/tlienart/Franklin.jl/pull/370/diff?src=pr&el=tree#diff-c3JjL2NvbnZlcnRlci9tYXJrZG93bi9tZC5qbA==) | `100% <100%> (ø)` | :arrow_up: |
| [src/parser/html/tokens.jl](https://codecov.io/gh/tlienart/Franklin.jl/pull/370/diff?src=pr&el=tree#diff-c3JjL3BhcnNlci9odG1sL3Rva2Vucy5qbA==) | `100% <100%> (ø)` | :arrow_up: |
| [src/converter/html/html.jl](https://codecov.io/gh/tlienart/Franklin.jl/pull/370/diff?src=pr&el=tree#diff-c3JjL2NvbnZlcnRlci9odG1sL2h0bWwuamw=) | `98.03% <100%> (-0.33%)` | :arrow_down: |
| [src/converter/markdown/utils.jl](https://codecov.io/gh/tlienart/Franklin.jl/pull/370/diff?src=pr&el=tree#diff-c3JjL2NvbnZlcnRlci9tYXJrZG93bi91dGlscy5qbA==) | `100% <100%> (ø)` | :arrow_up: |
| [src/parser/html/blocks.jl](https://codecov.io/gh/tlienart/Franklin.jl/pull/370/diff?src=pr&el=tree#diff-c3JjL3BhcnNlci9odG1sL2Jsb2Nrcy5qbA==) | `93.1% <100%> (+0.51%)` | :arrow_up: |
| [src/converter/html/blocks.jl](https://codecov.io/gh/tlienart/Franklin.jl/pull/370/diff?src=pr&el=tree#diff-c3JjL2NvbnZlcnRlci9odG1sL2Jsb2Nrcy5qbA==) | `95.87% <95.87%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/tlienart/Franklin.jl/pull/370?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/tlienart/Franklin.jl/pull/370?src=pr&el=footer). Last update [6357d3d...b951ae9](https://codecov.io/gh/tlienart/Franklin.jl/pull/370?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/Project.toml b/Project.toml
index 92b6f6c3..50ac5efa 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.3"
+version = "0.6.4"
 
 [deps]
 Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
diff --git a/src/Franklin.jl b/src/Franklin.jl
index e71b3618..d160add5 100644
--- a/src/Franklin.jl
+++ b/src/Franklin.jl
@@ -146,6 +146,7 @@ include("converter/latex/io.jl")
 # > html
 include("converter/html/functions.jl")
 include("converter/html/html.jl")
+include("converter/html/blocks.jl")
 include("converter/html/link_fixer.jl")
 include("converter/html/prerender.jl")
 
diff --git a/src/converter/html/blocks.jl b/src/converter/html/blocks.jl
new file mode 100644
index 00000000..2d903ac2
--- /dev/null
+++ b/src/converter/html/blocks.jl
@@ -0,0 +1,213 @@
+"""
+$SIGNATURES
+
+Recursively process a conditional block from an opening HTML_COND_OPEN to a
+{{end}}.
+"""
+function process_html_cond(hs::AS, qblocks::Vector{AbstractBlock},
+                           i::Int)::Tuple{String,Int,Int}
+    if i == length(qblocks)
+        throw(HTMLBlockError("Could not close the conditional block " *
+                             "starting with '$(qblocks[i].ss)'."))
+    end
+
+    init_idx      = i
+    else_idx      = 0
+    elseif_idx    = Vector{Int}()
+    accept_elseif = true # false as soon as we see an else block
+    content       = ""
+
+    # inbalance keeps track of whether we've managed to find a
+    # matching {{end}}. It increases if it sees other opening {{if..}}
+    # and decreases if it sees a {{end}}
+    inbalance = 1
+    probe = qblocks[i] # just for allocation
+    while i < length(qblocks) && inbalance > 0
+        i    += 1
+        probe = qblocks[i]
+        # if we have a {{elseif ...}} or {{else}} with the right level,
+        # keep track of them
+        if inbalance == 1
+            if probe isa HElseIf
+                accept_elseif || throw(HTMLBlockError("Saw a {{elseif ...}} " *
+                                            "after a {{else}} block."))
+                push!(elseif_idx, i)
+            elseif probe isa HElse
+                else_idx      = i
+                accept_elseif = false
+            end
+        end
+        inbalance += hbalance(probe)
+    end
+    # we've exhausted the candidate qblocks and not found an appropriate {{end}}
+    if inbalance > 0
+        throw(HTMLBlockError("Could not close the conditional block " *
+                             "starting with '$(qblocks[init_idx].ss)'."))
+    end
+
+    # we've found the closing {{end}} at index `i`
+    β_close = probe
+    i_close = i
+
+    # We now have a complete conditional block with possibly
+    # several else-if blocks and possibly an else block.
+    # Check which one applies and discard the rest
+
+    k   = 0 # index of the first verified condition
+    lag = 0 # aux var to distinguish 1st block case
+
+    # initial block may be {{if..}} or {{isdef..}} or...
+    # if its not just a {{if...}}, need to act appropriately
+    # and if the condition is verified then k=1
+    βi = qblocks[init_idx]
+    if βi isa Union{HIsDef,HIsNotDef,HIsPage,HIsNotPage}
+        lag = 1
+        if βi isa HIsDef
+            k = Int(haskey(LOCAL_VARS, βi.vname))
+        elseif βi isa HIsNotDef
+            k = Int(!haskey(LOCAL_VARS, βi.vname))
+        else
+            # HIsPage//HIsNotPage
+            rpath = splitext(unixify(locvar("fd_rpath")))[1]
+            if FD_ENV[:STRUCTURE] < v"0.2"
+                # current path is relative to /src/ for instance
+                # /src/pages/blah.md -> pages/blah
+                # if starts with `pages/`, replaces by `pub/`:
+                # pages/blah => pub/blah
+                rpath = replace(rpath, Regex("^pages") => "pub")
+            end
+            # compare with β.pages
+            inpage = any(p -> match_url(rpath, p), βi.pages)
+
+            if βi isa HIsPage
+                k = Int(inpage)
+            else
+                k = Int(!inpage)
+            end
+        end
+    end
+
+    # If we've not yet found a verified condition, keep looking
+    # Now all cond blocks ahead are {{if ...}} {{elseif  ...}}
+    if iszero(k)
+        if lag == 0
+            conds = [βi.vname, (qblocks[c].vname for c in elseif_idx)...]
+        else
+            conds = [qblocks[c].vname for c in elseif_idx]
+        end
+        known = [haskey(LOCAL_VARS, c) for c in conds]
+
+        if !all(known)
+            idx = findfirst(.!known)
+            throw(HTMLBlockError("At least one of the condition variable " *
+                                 "could not be found: check '$(conds[idx])'."))
+        end
+        # check that all these variables are bool
+        bools = [isa(locvar(c), Bool) for c  in conds]
+        if !all(bools)
+            idx = findfirst(.!bools)
+            throw(HTMLBlockError("At least one of the condition variable is " *
+                                 " not a Bool: check '$(conds[idx])'."))
+        end
+        # which one is verified?
+        u = findfirst(c -> locvar(c), conds)
+        k = isnothing(u) ? 0 : u + lag
+    end
+
+    if iszero(k)
+        # if we still haven't found a verified condition, use the else
+        # if there is one
+        if !iszero(else_idx)
+            # use elseblock, the content is from it to the β_close
+            head = nextind(hs, to(qblocks[else_idx]))
+            tail = prevind(hs, from(β_close))
+            # get the stack of blocks in there
+            action_qblocks = qblocks[else_idx+1:i_close-1]
+            # process
+            content = process_html_qblocks(hs, action_qblocks, head, tail)
+        end
+    else
+        # determine the span of blocks
+        oidx = 0 # opening conditional
+        cidx = 0 # closing conditional
+        if k == 1
+            oidx = init_idx
+        else
+            oidx = elseif_idx[k-1]
+        end
+        if k ≤ length(elseif_idx)
+            cidx = elseif_idx[k]
+        else
+            cidx = ifelse(iszero(else_idx), i_close, else_idx)
+        end
+        # Get the content
+        head = nextind(hs, to(qblocks[oidx]))
+        tail = prevind(hs, from(qblocks[cidx]))
+        # Get the stack of blocks in there
+        action_qblocks = qblocks[oidx+1:cidx-1]
+        # process
+        content = process_html_qblocks(hs, action_qblocks, head, tail)
+    end
+    # move the head after the final {{end}}
+    head = nextind(hs, to(β_close))
+
+    return content, head, i_close
+end
+
+"""
+$SIGNATURES
+
+Process a for block.
+"""
+function process_html_for(hs::AS, qblocks::Vector{AbstractBlock},
+                          i::Int)::Tuple{String,Int,Int}
+    # check that the iterable is known
+    β_open = qblocks[i]
+    vname  = β_open.vname
+    iname  = β_open.iname
+    if !haskey(LOCAL_VARS, iname)
+        throw(HTMLBlockError("The iterable '$iname' is not recognised. " *
+                             "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)'."))
+    end
+
+    init_idx = i
+    content  = ""
+
+    # inbalance keeps track of whether we've managed to find a
+    # matching {{end}}. It increases if it sees other opening {{if..}}
+    # and decreases if it sees a {{end}}
+    inbalance = 1
+    while i < length(qblocks) && inbalance > 0
+        i         += 1
+        inbalance += hbalance(qblocks[i])
+    end
+    # we've exhausted the candidate qblocks and not found an appropriate {{end}}
+    if inbalance > 0
+        throw(HTMLBlockError("Could not close the conditional block " *
+                             "starting with '$(qblocks[init_idx].ss)'."))
+    end
+    # we've found the closing {{end}} and index `i`
+    β_close = qblocks[i]
+    i_close = i
+
+    # 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.
+    inner = subs(hs, nextind(hs, to(β_open)), prevind(hs, from(β_close)))
+    content = ""
+    for value in locvar(iname)
+        # at the moment we only consider {{fill ...}}
+        content *= replace(inner,
+                    Regex("({{\\s*fill\\s+$vname\\s*}})") => "$value")
+    end
+    head = nextind(hs, to(β_close))
+    return convert_html(content), head, i_close
+end
diff --git a/src/converter/html/html.jl b/src/converter/html/html.jl
index fe2d20f7..ab11947e 100644
--- a/src/converter/html/html.jl
+++ b/src/converter/html/html.jl
@@ -60,6 +60,8 @@ fd2html(a...; k...)::String = fd2html_v(a...; k...)[1]
 jd2html = fd2html
 
 """
+$SIGNATURES
+
 Take a qualified html block stack and go through it, with recursive calling.
 """
 function process_html_qblocks(hs::AS, qblocks::Vector{AbstractBlock},
@@ -76,6 +78,9 @@ function process_html_qblocks(hs::AS, qblocks::Vector{AbstractBlock},
         if β isa HTML_OPEN_COND
             content, head, i = process_html_cond(hs, qblocks, i)
             write(htmls, content)
+        elseif β isa HFor
+            content, head, i = process_html_for(hs, qblocks, i)
+            write(htmls, content)
         # should not see an HEnd by itself --> error
         elseif β isa HEnd
             throw(HTMLBlockError("I found a lonely {{end}}."))
@@ -107,156 +112,3 @@ function match_url(base::AS, cand::AS)
     end
     return splitext(scand)[1] == sbase
 end
-
-
-"""
-Recursively process a conditional block from an opening HTML_COND_OPEN to a {{end}}.
-"""
-function process_html_cond(hs::AS, qblocks::Vector{AbstractBlock},
-                           i::Int)::Tuple{String,Int,Int}
-    if i == length(qblocks)
-        throw(HTMLBlockError("Could not close the conditional block " *
-                             "starting with '$(qblocks[i].ss)'."))
-    end
-
-    init_idx      = i
-    else_idx      = 0
-    elseif_idx    = Vector{Int}()
-    accept_elseif = true # false as soon as we see an else block
-    content       = ""
-
-    # inbalance keeps track of whether we've managed to find a
-    # matching {{end}}. It increases if it sees other opening {{if..}}
-    # and decreases if it sees a {{end}}
-    inbalance = 1
-    probe = qblocks[i] # just for allocation
-    while i < length(qblocks) && inbalance > 0
-        i    += 1
-        probe = qblocks[i]
-        # if we have a {{elseif ...}} or {{else}} with the right level,
-        # keep track of them
-        if inbalance == 1
-            if probe isa HElseIf
-                accept_elseif || throw(HTMLBlockError("Saw a {{elseif ...}} " *
-                                            "after a {{else}} block."))
-                push!(elseif_idx, i)
-            elseif probe isa HElse
-                else_idx      = i
-                accept_elseif = false
-            end
-        end
-        inbalance += hbalance(probe)
-    end
-    # we've exhausted the candidate qblocks and not found an appropriate {{end}}
-    if inbalance > 0
-        throw(HTMLBlockError("Could not close the conditional block " *
-                             "starting with '$(qblocks[init_idx].ss)'."))
-    end
-
-    # we've found the closing {{end}} at index `i`
-    β_close = probe
-    i_close = i
-
-    # We now have a complete conditional block with possibly
-    # several else-if blocks and possibly an else block.
-    # Check which one applies and discard the rest
-
-    k   = 0 # index of the first verified condition
-    lag = 0 # aux var to distinguish 1st block case
-
-    # initial block may be {{if..}} or {{isdef..}} or...
-    # if its not just a {{if...}}, need to act appropriately
-    # and if the condition is verified then k=1
-    βi = qblocks[init_idx]
-    if βi isa Union{HIsDef,HIsNotDef,HIsPage,HIsNotPage}
-        lag = 1
-        if βi isa HIsDef
-            k = Int(haskey(LOCAL_VARS, βi.vname))
-        elseif βi isa HIsNotDef
-            k = Int(!haskey(LOCAL_VARS, βi.vname))
-        else
-            # HIsPage//HIsNotPage
-            rpath = splitext(unixify(locvar("fd_rpath")))[1]
-            if FD_ENV[:STRUCTURE] < v"0.2"
-                # current path is relative to /src/ for instance
-                # /src/pages/blah.md -> pages/blah
-                # if starts with `pages/`, replaces by `pub/`:
-                # pages/blah => pub/blah
-                rpath = replace(rpath, Regex("^pages") => "pub")
-            end
-            # compare with β.pages
-            inpage = any(p -> match_url(rpath, p), βi.pages)
-
-            if βi isa HIsPage
-                k = Int(inpage)
-            else
-                k = Int(!inpage)
-            end
-        end
-    end
-
-    # If we've not yet found a verified condition, keep looking
-    # Now all cond blocks ahead are {{if ...}} {{elseif  ...}}
-    if iszero(k)
-        if lag == 0
-            conds = [βi.vname, (qblocks[c].vname for c in elseif_idx)...]
-        else
-            conds = [qblocks[c].vname for c in elseif_idx]
-        end
-        known = [haskey(LOCAL_VARS, c) for c in conds]
-
-        if !all(known)
-            idx = findfirst(.!known)
-            throw(HTMLBlockError("At least one of the condition variable could not be found: " *
-                                 "couldn't find '$(conds[idx])'."))
-        end
-        # check that all these variables are bool
-        bools = [isa(locvar(c), Bool) for c  in conds]
-        if !all(bools)
-            idx = findfirst(.!bools)
-            throw(HTMLBlockError("At least one of the condition variable is not a Bool: " *
-                                 "'$(conds[idx])' is not a bool."))
-        end
-        # which one is verified?
-        u = findfirst(c -> locvar(c), conds)
-        k = isnothing(u) ? 0 : u + lag
-    end
-
-    if iszero(k)
-        # if we still haven't found a verified condition, use the else if there is one
-        if !iszero(else_idx)
-            # use elseblock, the content is from it to the β_close
-            head = nextind(hs, to(qblocks[else_idx]))
-            tail = prevind(hs, from(β_close))
-            # get the stack of blocks in there
-            action_qblocks = qblocks[else_idx+1:i_close-1]
-            # process
-            content = process_html_qblocks(hs, action_qblocks, head, tail)
-        end
-    else
-        # determine the span of blocks
-        oidx = 0 # opening conditional
-        cidx = 0 # closing conditional
-        if k == 1
-            oidx = init_idx
-        else
-            oidx = elseif_idx[k-1]
-        end
-        if k ≤ length(elseif_idx)
-            cidx = elseif_idx[k]
-        else
-            cidx = ifelse(iszero(else_idx), i_close, else_idx)
-        end
-        # Get the content
-        head = nextind(hs, to(qblocks[oidx]))
-        tail = prevind(hs, from(qblocks[cidx]))
-        # Get the stack of blocks in there
-        action_qblocks = qblocks[oidx+1:cidx-1]
-        # process
-        content = process_html_qblocks(hs, action_qblocks, head, tail)
-    end
-    # move the head after the final {{end}}
-    head = nextind(hs, to(β_close))
-
-    return content, head, i_close
-end
diff --git a/src/converter/markdown/md.jl b/src/converter/markdown/md.jl
index ba387143..27ce47e0 100644
--- a/src/converter/markdown/md.jl
+++ b/src/converter/markdown/md.jl
@@ -53,6 +53,15 @@ function convert_md(mds::AS,
     #> 1b. Find indented blocks (ONLY if not recursive to avoid ambiguities!)
     if !isrecursive
         find_indented_blocks!(tokens, mds)
+        if has_mddefs
+            # look for multi-line definitions, for those that meet the
+            # requirements, discard *all* tokens in the span leaving only
+            # the opening and closing one. Note: expressions will be parsed
+            # twice as a result but really it's negligible time.
+            preprocess_candidate_mddefs!(tokens)
+        end
+        # any remaining indented lines that don't meet the requirements for
+        # an indented code block get removed here
         filter_lr_indent!(tokens, mds)
     end
 
@@ -374,17 +383,17 @@ function process_mddefs(blocks::Vector{OCBlock}, isconfig::Bool)::Nothing
     assignments = Vector{Pair{String, String}}()
     # go over the blocks, and extract the assignment
     for (i, mdd) ∈ enumerate(mddefs)
-        matched = match(MD_DEF_PAT, mdd.ss)
-        if isnothing(matched)
+        inner = strip(content(mdd))
+        m = match(ASSIGN_PAT, inner)
+        if isnothing(m)
             @warn "Found delimiters for an @def environment but it didn't " *
                   "have the right @def var = ... format. Verify (ignoring " *
                   "for now)."
             continue
         end
-        vname, vdef = matched.captures[1:2]
+        vname, vdef = m.captures[1:2]
         push!(assignments, (String(vname) => String(vdef)))
     end
-
     # if in config file, update `GLOBAL_VARS` and `GLOBAL_LXDEFS`
     if isconfig
         set_vars!(GLOBAL_VARS, assignments)
diff --git a/src/converter/markdown/utils.jl b/src/converter/markdown/utils.jl
index cea3030e..428af064 100644
--- a/src/converter/markdown/utils.jl
+++ b/src/converter/markdown/utils.jl
@@ -59,3 +59,59 @@ extraneous whitespace.
 fix_inserts(s::AS)::String =
     replace(replace(s, r"([\*_]) ##FDINSERT##" => s"\1##FDINSERT##"),
                        r"##FDINSERT## ([\*_])" => s"##FDINSERT##\1")
+
+"""
+$(SIGNATURES)
+
+Takes a list of tokens and deactivate tokens that happen to be in a multi-line
+md-def. Used in [`convert_md`](@ref).
+"""
+function preprocess_candidate_mddefs!(tokens::Vector{Token})
+    isempty(tokens) && return nothing
+    # process:
+    # 1. find a MD_DEF_OPEN token
+    # 2. Look for the first LINE_RETURN (proper)
+    # 3. try to parse the content with Meta.parse. If it completely fails,
+    # error, otherwise consider the  span of the first ok expression and
+    # discard all tokens within its span leaving effectively just the
+    # opening MD_DEF_OPEN and closing LINE_RETURN
+    from_to_list = Pair{Int,Int}[]
+    i = 0
+    while i < length(tokens)
+        i += 1
+        τ  = tokens[i]
+        τ.name == :MD_DEF_OPEN || continue
+        # look ahead stopping with the first LINE_RETURN
+        j = findfirst(τc -> τc.name ∈ (:LINE_RETURN, :EOS), tokens[i+1:end])
+        if j !== nothing
+            e = i+j
+            # discard if it's a single line
+            any(τx -> τx.name == :LR_INDENT, tokens[i:e]) || continue
+            push!(from_to_list, (i => e))
+        end
+    end
+    remove = Int[]
+    s = str(first(tokens))
+    for (i, j) in from_to_list
+        si = nextind(s, to(tokens[i]))
+        sj = prevind(s, from(tokens[j]))
+        sc = subs(s, si, sj)
+        ex, pos = Meta.parse(sc, 1)
+
+        # find where's the first character after the effective end of the
+        # definition (right-strip)
+        str_expr = subs(s, si, si + prevind(sc, pos))
+        stripped = strip(str_expr)
+        start_id = findfirst(c -> c == stripped[1], str_expr)
+        last_id  = prevind(str_expr, start_id + length(stripped))
+
+        # find the first token after si + next_id - 1 and discard what's before
+        c = findfirst(k -> from(tokens[k]) > si + last_id - 1, i+1:j)
+        # we discard everything between i+1 and i+c-1 (we want to keep i+c)
+        if !isnothing(c)
+            append!(remove, i+1:i+c-1)
+        end
+    end
+    deleteat!(tokens, remove)
+    return nothing
+end
diff --git a/src/parser/html/blocks.jl b/src/parser/html/blocks.jl
index 3852fe09..3b2575d9 100644
--- a/src/parser/html/blocks.jl
+++ b/src/parser/html/blocks.jl
@@ -37,6 +37,10 @@ function qualify_html_hblocks(blocks::Vector{OCBlock})::Vector{AbstractBlock}
         m = match(HBLOCK_ISNOTPAGE_PAT, β.ss)
         isnothing(m) || (qb[i] = HIsNotPage(β.ss, split(m.captures[1])); continue)
         # ---
+        # for block
+        m = match(HBLOCK_FOR_PAT, β.ss)
+        isnothing(m) || (qb[i] = HFor(β.ss, m.captures[1], m.captures[2]); continue)
+        # ---
         # function block {{ fname v1 v2 ... }}
         m = match(HBLOCK_FUN_PAT, β.ss)
         isnothing(m) || (qb[i] = HFun(β.ss, m.captures[1], split(m.captures[2])); continue)
diff --git a/src/parser/html/tokens.jl b/src/parser/html/tokens.jl
index 59886b14..5cefdf0f 100644
--- a/src/parser/html/tokens.jl
+++ b/src/parser/html/tokens.jl
@@ -49,25 +49,27 @@ at this point. This second point might fix the first one by making sure that
 =#
 
 """
-HBLOCK_IF_PAT
-HBLOCK_ELSE_PAT
-HBLOCK_ELSEIF_PAT
-HBLOCK_END_PAT
-HBLOCK_ISDEF_PAT
-HBLOCK_ISNOTDEF_PAT
-HBLOCK_ISPAGE_PAT
-HBLOCK_ISNOTPAGE_PAT
+HBLOCK_IF_PAT           # {{if v1}}
+HBLOCK_ELSE_PAT         # {{else}}
+HBLOCK_ELSEIF_PAT       # {{elseif v1}}
+HBLOCK_END_PAT          # {{end}}
+HBLOCK_ISDEF_PAT        # {{isdef v1}}
+HBLOCK_ISNOTDEF_PAT     # {{isnotdef v1}}
+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*}}"        # {{if v1}}
-const HBLOCK_ELSE_PAT      = r"{{\s*else\s*}}"                      # {{else}}
-const HBLOCK_ELSEIF_PAT    = r"{{\s*else\s*if\s+([a-zA-Z]\S*)\s*}}" # {{elseif v1}}
-const HBLOCK_END_PAT       = r"{{\s*end\s*}}"                       # {{end}}
-const HBLOCK_ISDEF_PAT     = r"{{\s*isdef\s+([a-zA-Z]\S*)\s*}}"     # {{isdef v1}}
-const HBLOCK_ISNOTDEF_PAT  = r"{{\s*isnotdef\s+([a-zA-Z]\S*)\s*}}"  # {{isnotdef v1}}
-const HBLOCK_ISPAGE_PAT    = r"{{\s*ispage\s+((.|\n)+?)}}"          # {{ispage p1 p2}}
-const HBLOCK_ISNOTPAGE_PAT = r"{{\s*isnotpage\s+((.|\n)+?)}}"       # {{isnotpage p1 p2}}
+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*isdef\s+([a-zA-Z_]\S*)\s*}}"
+const HBLOCK_ISNOTDEF_PAT  = r"{{\s*isnotdef\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)
@@ -174,6 +176,17 @@ struct HIsNotPage <: AbstractBlock
     pages::Vector{<:AS}
 end
 
+"""
+$(TYPEDEF)
+
+HTML token corresponding to `{{for x in iterable}}`.
+"""
+struct HFor <: AbstractBlock
+    ss::SubString
+    vname::String
+    iname::String
+end
+
 #= ============
 FUNCTION BLOCKS
 =============== =#
diff --git a/src/parser/markdown/tokens.jl b/src/parser/markdown/tokens.jl
index 27ffabca..c8246b00 100644
--- a/src/parser/markdown/tokens.jl
+++ b/src/parser/markdown/tokens.jl
@@ -108,17 +108,14 @@ const MD_TOKENS_LX = Dict{Char, Vector{TokenFinder}}(
         incrlook((_, c) -> α(c)) => :LX_COMMAND ]
     )
 
-
 """
-MD_DEF_PAT
-
-Regex to match an assignment of the form
+    ASSIGN_PAT
 
-    @def var = value
+Regex to match 'var' in an assignment of the form
 
-The first group captures the name (`var`), the second the assignment (`value`).
+    var = value
 """
-const MD_DEF_PAT = r"@def\s+(\S+)\s*?=\s*?(\S.*)"
+const ASSIGN_PAT = r"^([a-zA-Z_]\S*)\s*?=((?:.|\n)*)"
 
 
 """
@@ -150,9 +147,10 @@ const MD_OCB = [
     OCProto(:CODE_BLOCK_LANG, :CODE_LANG2,   (:CODE_PENTA,),    false),
     OCProto(:CODE_BLOCK,      :CODE_TRIPLE,  (:CODE_TRIPLE,),   false),
     OCProto(:CODE_BLOCK,      :CODE_PENTA,   (:CODE_PENTA,),    false),
-    OCProto(:CODE_BLOCK_IND,  :LR_INDENT,    (:LINE_RETURN,),   false),
     OCProto(:CODE_INLINE,     :CODE_DOUBLE,  (:CODE_DOUBLE,),   false),
     OCProto(:CODE_INLINE,     :CODE_SINGLE,  (:CODE_SINGLE,),   false),
+    OCProto(:MD_DEF,          :MD_DEF_OPEN,  L_RETURNS,         false), # [^4]
+    OCProto(:CODE_BLOCK_IND,  :LR_INDENT,    (:LINE_RETURN,),   false),
     OCProto(:ESCAPE,          :ESCAPE,       (:ESCAPE,),        false),
     OCProto(:FOOTNOTE_DEF,    :FOOTNOTE_DEF, L_RETURNS,         false),
     OCProto(:LINK_DEF,        :LINK_DEF,     L_RETURNS,         false),
@@ -164,7 +162,6 @@ const MD_OCB = [
     OCProto(:H5,              :H5_OPEN,      L_RETURNS,     false),
     OCProto(:H6,              :H6_OPEN,      L_RETURNS,     false),
     # ------------------------------------------------------------------
-    OCProto(:MD_DEF,          :MD_DEF_OPEN,  L_RETURNS,     false), # see [^4]
     OCProto(:LXB,             :LXB_OPEN,     (:LXB_CLOSE,), true ),
     OCProto(:DIV,             :DIV_OPEN,     (:DIV_CLOSE,), true ),
     ]
@@ -172,8 +169,8 @@ const MD_OCB = [
 * [3] a header can be closed by either a line return or an end of string (for
 instance in the case where a user defines a latex command like so:
 \newcommand{\section}{# blah} (no line return).)
-* [4] an `MD_DEF` goes from an `@def` to the next `\n` so no multiple-line
-def are allowed.
+* [4] MD_DEF take precedence over CODE_IND, note that if you have an indented
+* block with @def in it, things may go bad.
 * ordering matters!
 =#
 
diff --git a/src/utils/vars.jl b/src/utils/vars.jl
index 3befcc77..5dfa58ea 100644
--- a/src/utils/vars.jl
+++ b/src/utils/vars.jl
@@ -223,7 +223,7 @@ function set_vars!(vars::PageVars, assignments::Vector{Pair{String,String}}
         # this in a string it would fail (but come on...)
         idx = findfirst("<!--", assign)
         !isnothing(idx) && (assign = assign[1:prevind(assign, idx[1])])
-        tmp = Meta.parse("__tmp__ = " * assign)
+        tmp, = Meta.parse(assign, 1)
         # try to evaluate the parsed assignment
         try
             tmp = eval(tmp)


Test patch:
diff --git a/test/converter/html_for.jl b/test/converter/html_for.jl
new file mode 100644
index 00000000..9f4d036e
--- /dev/null
+++ b/test/converter/html_for.jl
@@ -0,0 +1,41 @@
+@testset "h-for" begin
+    F.def_LOCAL_VARS!()
+    s = """
+        @def list = [1,2,3]
+        """ |> fd2html_td
+    hs = raw"""
+        ABC
+        {{for x in list}}
+          {{fill x}}
+        {{end}}
+        """
+    tokens = F.find_tokens(hs, F.HTML_TOKENS, F.HTML_1C_TOKENS)
+    hblocks, tokens = F.find_all_ocblocks(tokens, F.HTML_OCB)
+    qblocks = F.qualify_html_hblocks(hblocks)
+    @test qblocks[1] isa F.HFor
+    @test qblocks[1].vname == "x"
+    @test qblocks[1].iname == "list"
+    @test qblocks[2] isa F.HFun
+    @test qblocks[3] isa F.HEnd
+
+    content, head, i = F.process_html_for(hs, qblocks, 1)
+    @test isapproxstr(content, "1 2 3")
+end
+
+@testset "h-for2" begin
+    F.def_LOCAL_VARS!()
+    s = """
+        @def list = ["path/to/badge1.png", "path/to/badge2.png"]
+        """ |> fd2html_td
+    h = raw"""
+        ABC
+        {{for x in list}}
+            {{fill x}}
+        {{end}}
+        """ |> F.convert_html
+    @test isapproxstr(h, """
+        ABC
+        path/to/badge1.png
+        path/to/badge2.png
+        """)
+end
diff --git a/test/converter/md_defs.jl b/test/converter/md_defs.jl
new file mode 100644
index 00000000..fd4bf75b
--- /dev/null
+++ b/test/converter/md_defs.jl
@@ -0,0 +1,90 @@
+@testset "preprocess" begin
+    s = """
+        @def z = [1,2,3,
+            4,5,6]
+        """
+    tokens = F.find_tokens(s, F.MD_TOKENS, F.MD_1C_TOKENS)
+    F.find_indented_blocks!(tokens, s)
+
+    @test tokens[1].name == :MD_DEF_OPEN
+    @test tokens[2].name == :LR_INDENT
+    @test tokens[3].name == :LINE_RETURN
+    @test tokens[4].name == :EOS
+
+    F.preprocess_candidate_mddefs!(tokens)
+
+    @test tokens[1].name == :MD_DEF_OPEN
+    @test tokens[2].name == :LINE_RETURN
+    @test tokens[3].name == :EOS
+
+    blocks, = F.find_all_ocblocks(tokens, F.MD_OCB_ALL)
+
+    content = F.content.(blocks) .|> String
+    @test isapproxstr(content[1], "z = [1,2,3,4,5,6]")
+
+    s = """
+        @def z1 = [1,2,3,
+            4,5,6]
+        @def z2 = 3
+        @def z3 = [1,
+            2,
+                3]
+        """
+    tokens = F.find_tokens(s, F.MD_TOKENS, F.MD_1C_TOKENS)
+    F.find_indented_blocks!(tokens, s)
+    seqtok = (:MD_DEF_OPEN, :LR_INDENT, :LINE_RETURN,
+              :MD_DEF_OPEN, :LINE_RETURN,
+              :MD_DEF_OPEN, :LR_INDENT, :LR_INDENT, :LINE_RETURN,
+              :EOS)
+    for i in 1:length(tokens)
+        @test tokens[i].name == seqtok[i]
+    end
+
+    F.preprocess_candidate_mddefs!(tokens)
+    seqtok = (:MD_DEF_OPEN, :LINE_RETURN,
+              :MD_DEF_OPEN, :LINE_RETURN,
+              :MD_DEF_OPEN, :LINE_RETURN,
+              :EOS)
+    for i in 1:length(tokens)
+        @test tokens[i].name == seqtok[i]
+    end
+
+    blocks, = F.find_all_ocblocks(tokens, F.MD_OCB_ALL)
+    content = F.content.(blocks) .|> String
+    @test isapproxstr(content[1], "z1=[1,2,3,4,5,6]")
+    @test isapproxstr(content[2], "z2=3")
+    @test isapproxstr(content[3], "z3=[1,2,3]")
+end
+
+
+@testset "mddefs1" begin
+    F.def_LOCAL_VARS!()
+    s = """
+        @def x = 5
+        """ |> fd2html_td
+    @test F.locvar("x") == 5
+    s = """
+        @def x = "hello"; <!-- nothing -->
+        """ |> fd2html_td
+    @test F.locvar("x") == "hello"
+    s = """
+        @def x = "hello"
+        @def y = pi
+        """ |> fd2html_td
+    @test F.locvar("x") == "hello"
+    @test F.locvar("y") == pi
+    s = """
+        @def z = [1,2,3,
+                  4,5,6]
+        A
+        """ |> fd2html_td
+    @test F.locvar("z") == collect(1:6)
+    s = """
+        @def s = \"\"\"foo
+            bar
+            baz etc\"\"\"
+        @def s2 = "nothing"
+        """ |> fd2html_td
+    @test isapproxstr(F.locvar("s"), "foo bar baz etc")
+    @test isapproxstr(F.locvar("s2"), "nothing")
+end
diff --git a/test/parser/markdown+latex.jl b/test/parser/markdown+latex.jl
index acfa2e39..6546e84d 100644
--- a/test/parser/markdown+latex.jl
+++ b/test/parser/markdown+latex.jl
@@ -217,10 +217,10 @@ end
 
     @test blocks[1].name == :COMMENT
     @test F.content(blocks[1]) == " comment "
-    @test blocks[2].name == :H2
-    @test F.content(blocks[2]) == " blah <!-- ✅ 19/9/999 -->"
-    @test blocks[3].name == :MD_DEF
-    @test F.content(blocks[3]) == " title = \"Convex Optimisation I\""
+    @test blocks[3].name == :H2
+    @test F.content(blocks[3]) == " blah <!-- ✅ 19/9/999 -->"
+    @test blocks[2].name == :MD_DEF
+    @test F.content(blocks[2]) == " title = \"Convex Optimisation I\""
 
     @test lxcoms[1].ss == "\\com{A}"
     @test lxcoms[2].ss == "\\com{B}"
diff --git a/test/runtests.jl b/test/runtests.jl
index 967fb102..01653114 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -51,10 +51,12 @@ include("converter/markdown2.jl")
 include("converter/markdown3.jl")
 include("converter/markdown4.jl")
 include("converter/hyperref.jl")
+include("converter/md_defs.jl")
 println("🍺")
 println("CONVERTER/HTML")
 include("converter/html.jl")
 include("converter/html2.jl")
+include("converter/html_for.jl")
 println("🍺")
 println("CONVERTER/LX")
 include("converter/lx_input.jl")