Repository: tlienart/Franklin.jl
Pull request #481 (2020-05-08 17:13:34)

Taxonomy (second attempt)
Alright folks, this brings **tags** (back) in. When I have gained more confidence that this works as expected I'll merge and it will be minor release `0.8`. This is one of the main missing feature that I really wanted to add in Franklin.

I'd like some help to check it has the right behaviour. Basic stuff is unit-tested but the overall working of the tags is a bit subtle in terms of cleaning things up when tags are added/removed while the server is running. 

### To test this

Just add `@def tags = [...]` on meaningful pages. The first run will force-add a basic `_layout/tag.html` page which will be the mould for all generate tag pages. You should tweak this so that it respects the style of the rest of your pages. In the future it will be part of templates. 
The function `{{taglist}}` could be replaced by your own function `{{customTaglist}}` by adding a `hfun_customTaglist` in your `utils.jl` but let's maybe not worry about that right now.

The basic one `{{taglist}}` just adds the list of pages corresponding to  the tag by reverse chronological order. 

The very important thing to note is that `_layout/tag.html` **must not depend on local page variable**. It can only:

* use global page variables
* use page variable via `{{fill var path/to/page}}` 

The only  exception is `fd_tag` which contains the tag name. This you can use in the title for instance. For convenience let's reproduce the default HTML with some comments:

```html
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Tag: {{fill fd_tag}}</title>
</head>
<body>
  <div class="{{div_content}} tagpage">
    {{taglist}}
  </div>
</body>
</html>
```

* You can see the `{{fill fd_tag}}` this will fill the tag name
* You can  see the `{{taglist}}` function which will generate something like

```html
<ul>
  <li><a href="/path/to/page_more_recent/">Title if it exists</a></li>
  <li><a href="/path/to/page_less_recent/">Title if it exists</a></li>
</ul>
```

Finally you can still use `{{ispage /tag/tagname/}}...{{end}}` to change the style depending on which tag page you're on.

Wikunia: Some comments:
I think the default `tag.html` in `_layout` should include the head and foot.
My setup:
```
{{insert head.html }}
  <div class="{{div_content}} tagpage">
    {{taglist}}
  </div>
{{insert foot.html}}
```

The links doesn't work for me i.e
A `href` linking to `/blog/constraint-solver-table-constraint/index/` is created but it should be `/blog/constraint-solver-table-constraint/` or with index.html in the end.

Adding, deleting tags seem to work for me ;)
tlienart: Thanks for the feedback! 

* link ending with `index`, yes I noticed that, simple fix coming
* adding head  and foot, so actually that cannot be the case because tag pages **must not** depend on local page var but, by default,  `head` and  `foot` will typically  have things like `{{fill title}}` etc which are ambiguous in generated pages. 

The new templates show how one can do this (e.g.: https://github.com/tlienart/FranklinTemplates.jl/tree/tags/src/templates/basic/_layout), basically make the content of `head` more modular, and plug those blocks also in `tag.html`.
Wikunia: I see yeah I've changed my foot and head accordingly but your decision makes sense then I guess.
The fix doesn't seem to have worked for me. I did `] add Franklin#tags2` again hope that should be enough
tlienart: Sorry the initial fix was something else, the actual fix will be in soon, thanks!
tlienart: Now in 😄
tlienart: I've updated all templates, will play with this a bit more and merge tomorrow I think.
Wikunia: Haha now the href is just `/` :rofl: 
tlienart: oops
tlienart: fixed... dumb typo

Patch:
diff --git a/Project.toml b/Project.toml
index 599bdf07..35146712 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.7.10"
+version = "0.8.0"
 
 [deps]
 Crayons = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f"
@@ -22,7 +22,7 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
 [compat]
 Crayons = "4"
 DocStringExtensions = "0.8"
-FranklinTemplates = "0.6"
+FranklinTemplates = "0.6,0.7"
 HTTP = "0.8"
 Literate = "2.2"
 LiveServer = "0.3"
diff --git a/src/Franklin.jl b/src/Franklin.jl
index 928bfe71..0e51c651 100644
--- a/src/Franklin.jl
+++ b/src/Franklin.jl
@@ -21,7 +21,7 @@ import HTTP
 import Random
 
 export serve, publish, cleanpull, newsite, optimize, fd2html,
-       literate_folder, verify_links, @OUTPUT
+       literate_folder, verify_links, @OUTPUT, get_url
 
 # Extra functions
 export lunr
diff --git a/src/converter/html/blocks.jl b/src/converter/html/blocks.jl
index 80a78a7d..aeff9af1 100644
--- a/src/converter/html/blocks.jl
+++ b/src/converter/html/blocks.jl
@@ -76,6 +76,16 @@ function process_html_cond(hs::AS, qblocks::Vector{AbstractBlock},
                 # pages/blah => pub/blah
                 rpath = replace(rpath, Regex("^pages") => "pub")
             end
+            # are we currently on a tag page?
+            if !isempty(locvar("fd_tag"))
+                tag = locvar("fd_tag")
+                if FD_ENV[:STRUCTURE] < v"0.2"
+                    rpath = "/pub/tag/$tag/"
+                else
+                    rpath = "/tag/$tag/"
+                end
+            end
+
             # compare with β.pages
             inpage = any(p -> match_url(rpath, p), βi.pages)
 
diff --git a/src/converter/html/functions.jl b/src/converter/html/functions.jl
index cae7d934..0711a6ab 100644
--- a/src/converter/html/functions.jl
+++ b/src/converter/html/functions.jl
@@ -188,21 +188,15 @@ end
 """
 $(SIGNATURES)
 
-H-Function of the form `{{list tag}}`.
+H-Function of the form `{{taglist}}`.
 """
-function hfun_list(params::Vector{String})::String
-    if length(params) != 1
-        throw(HTMLFunctionError("I found a {{list ...}} block and expected 1 " *
-                                "parameter but got $(length(params)). Verify."))
-    end
-    tag = params[1]
-    TAG_PAGES = globvar("fd_tag_pages")
+function hfun_taglist()::String
+    tag = locvar(:fd_tag)
 
     c = IOBuffer()
-    write(c, "<h1>Tag: $tag</h1>")
     write(c, "<ul>")
 
-    rpaths = TAG_PAGES[tag]
+    rpaths = globvar("fd_tag_pages")[tag]
     sorter(p) = begin
         pvd = pagevar(p, "date")
         if isnothing(pvd)
@@ -219,11 +213,10 @@ function hfun_list(params::Vector{String})::String
         if isnothing(title)
             title = "/$rpath/"
         end
-
-        (:hfun_list, "(in loop) $rpath - $title") |> logger
-
-        write(c, "<li><a href=\"/$rpath/\">$title</a></li>")
+        url = get_url(rpath)
+        write(c, "<li><a href=\"$url\">$title</a></li>")
     end
     write(c, "</ul>")
+
     return String(take!(c))
 end
diff --git a/src/converter/html/html.jl b/src/converter/html/html.jl
index 7b52febb..5f309c74 100644
--- a/src/converter/html/html.jl
+++ b/src/converter/html/html.jl
@@ -12,7 +12,8 @@ function convert_html(hs::AS; isoptim::Bool=false)::String
 
     # Find hblocks ({{ ... }})
     hblocks, tokens = find_all_ocblocks(tokens, HTML_OCB)
-    filter!(hb -> hb.name != :COMMENT, hblocks)
+    deactivate_inner_blocks!(hblocks, (:COMMENT, :SCRIPT))
+    filter!(hb -> hb.name ∉ (:COMMENT, :SCRIPT), hblocks)
 
     # Find qblocks (qualify the hblocks)
     qblocks = qualify_html_hblocks(hblocks)
diff --git a/src/converter/markdown/blocks.jl b/src/converter/markdown/blocks.jl
index 74d9934a..72475503 100644
--- a/src/converter/markdown/blocks.jl
+++ b/src/converter/markdown/blocks.jl
@@ -34,7 +34,7 @@ function convert_block(β::AbstractBlock, lxdefs::Vector{LxDef})::AS
         cont     = convert_md(raw_cont, lxdefs;
                               isrecursive=true, has_mddefs=false)
         divname  = chop(otok(β).ss, head=2, tail=0)
-        return html_div(divname, cont)
+        return html_div(replace(divname, ","=>" "), cont)
     end
 
     # default case, ignore block (should not happen)
diff --git a/src/converter/markdown/md.jl b/src/converter/markdown/md.jl
index 0ab9edb9..86bef2b6 100644
--- a/src/converter/markdown/md.jl
+++ b/src/converter/markdown/md.jl
@@ -70,7 +70,7 @@ function convert_md(mds::AS,
     #>> a'. find the rest
     blocks2, tokens = find_all_ocblocks(tokens, vcat(MD_OCB2, MD_OCB_MATH))
     append!(blocks, blocks2)
-    deactivate_inner_blocks!(blocks)
+    ranges = deactivate_inner_blocks!(blocks)
     #>> b. merge CODE_BLOCK_IND which are separated by emptyness
     merge_indented_blocks!(blocks, mds)
     #>> b'. only keep indented code blocks which are not contained in larger
@@ -102,7 +102,7 @@ function convert_md(mds::AS,
     #> 3[ex]. find double brace blocks, note we do it on pre_ocb tokens
     # as the step `find_all_ocblocks` possibly found and deactivated {...}.
     dbb = find_double_brace_blocks(toks_pre_ocb)
-
+    deactivate_inner_dbb!(dbb, ranges)
     # ------------------------------------------------------------------------
     #> 4. Page variable definition (mddefs), also if in config, update lxdefs
     if has_mddefs
@@ -238,6 +238,8 @@ const INSERT_LEN = length(INSERT_)
 
 
 """
+$SIGNATURES
+
 Form an intermediate MD file where special blocks are replaced by a marker
 (`INSERT`) indicating that a piece will need to be plugged in there later.
 
@@ -377,7 +379,8 @@ function convert_inter_html(ihtml::AS,
             head = ifelse(ihtml[head] in (' ', '>'), nextind(ihtml, head), head)
         end
         # store the resolved block
-        write(htmls, convert_block(blocks[i], lxdefs))
+        resolved = convert_block(blocks[i], lxdefs)
+        write(htmls, resolved)
     end
     # store whatever is after the last INSERT if anything
     (head ≤ strlen) && write(htmls, subs(ihtml, head:strlen))
diff --git a/src/converter/markdown/mddefs.jl b/src/converter/markdown/mddefs.jl
index ee9d70af..5815b490 100644
--- a/src/converter/markdown/mddefs.jl
+++ b/src/converter/markdown/mddefs.jl
@@ -63,39 +63,42 @@ function process_mddefs(blocks::Vector{OCBlock}, isconfig::Bool,
 
     (:process_mddefs, "assignments done & copy to ALL_PAGE_VARS") |> logger
 
-    # # TAGS
-    # tags = Set(unique(locvar("tags")))
-    # # Cases:
-    # # 1. that page did not have tags
-    # #   a. tags is empty --> do nothing
-    # #   b. tags is not empty register them and update all
-    # # 2. that page did have tags
-    # #   a. tags are unchanged --> do nothing
-    # #   b. check which ones change and update those
-    # PAGE_TAGS = globvar("fd_page_tags")
-    # if isnothing(PAGE_TAGS)
-    #     isempty(tags) && return nothing
-    #     set_var!(GLOBAL_VARS, "fd_page_tags", DTAG((rpath => tags,)))
-    # elseif !haskey(PAGE_TAGS, rpath)
-    #     isempty(tags) && return nothing
-    #     PAGE_TAGS[rpath] = tags
-    # else
-    #     old_tags = PAGE_TAGS[rpath]
-    #     if isempty(tags)
-    #         delete!(PAGE_TAGS, rpath)
-    #     else
-    #         PAGE_TAGS[rpath] = tags
-    #     end
-    #     # we will need to update all tag pages that were or are related to
-    #     # the current rpath, indeed properties of the page may have changed
-    #     # and affect these derived pages (that's why it's a union here
-    #     # and not a setdiff).
-    #     tags = union(old_tags, tags)
-    # end
-    # # In the full pass each page is processed first (without generating tag
-    # # pages) and then, when all tags have been gathered, generate_tag_pages
-    # # is called (see `fd_fullpass`).
-    # # During the serve loop, we want to trigger on page change.
-    # pagevar || FD_ENV[:FULL_PASS] || generate_tag_pages(tags)
+    # TAGS
+    tags = Set(unique(locvar("tags")))
+    # Cases:
+    # 0. there was no page tags before
+    #   a. tags is empty --> do nothing
+    #   b. tags is not empty --> initialise global page tags + add rpath=>tags
+    # 1. that page did not have tags
+    #   a. tags is empty --> do nothing
+    #   b. tags is not empty register them and update all
+    # 2. that page did have tags
+    #   a. tags are unchanged --> do nothing
+    #   b. check which ones change and update those
+    PAGE_TAGS = globvar("fd_page_tags")
+    if isnothing(PAGE_TAGS)
+        isempty(tags) && return nothing
+        set_var!(GLOBAL_VARS, "fd_page_tags", DTAG((rpath => tags,)))
+    elseif !haskey(PAGE_TAGS, rpath)
+        isempty(tags) && return nothing
+        PAGE_TAGS[rpath] = tags
+    else
+        old_tags = PAGE_TAGS[rpath]
+        if isempty(tags)
+            delete!(PAGE_TAGS, rpath)
+        else
+            PAGE_TAGS[rpath] = tags
+        end
+        # we will need to update all tag pages that were or are related to
+        # the current rpath, indeed properties of the page may have changed
+        # and affect these derived pages (that's why it's a union here
+        # and not a setdiff).
+        tags = union(old_tags, tags)
+    end
+    # In the full pass each page is processed first (without generating tag
+    # pages) and then, when all tags have been gathered, generate_tag_pages
+    # is called (see `fd_fullpass`).
+    # During the serve loop, we want to trigger on page change.
+    pagevar || FD_ENV[:FULL_PASS] || generate_tag_pages(tags)
     return nothing
 end
diff --git a/src/converter/markdown/tags.jl b/src/converter/markdown/tags.jl
index 6d9b9e81..33493049 100644
--- a/src/converter/markdown/tags.jl
+++ b/src/converter/markdown/tags.jl
@@ -6,42 +6,74 @@ refresh (in which case only the pages associated to those tags will be
 refreshed) or an empty set in which case all tags will be (re)-generated.
 Note that if a tag is removed from a page, it may be that `refresh_tags`
 contains a tag which should effectively be removed because no page have it.
-(This only matters at the end in the call to `write_tag_pages`).
+(This only matters at the end in the call to `write_tag_page`).
+Note: the `clean_tags` cleans up orphan tags (tags that wouldn't be pointing
+to any page anymore).
 """
 function generate_tag_pages(refresh_tags=Set{String}())::Nothing
-    # filter out pages that may not exist anymore
+    # if there are no page tags, cleanup and finish
     PAGE_TAGS = globvar("fd_page_tags")
-    isnothing(PAGE_TAGS) && return nothing
+    isnothing(PAGE_TAGS) && return clean_tags()
+    # if there are page tags, eliminiate the pages that don't exist anymore
     for rpath in keys(PAGE_TAGS)
         isfile(rpath * ".md") || delete!(PAGE_TAGS, rpath)
     end
-    isempty(PAGE_TAGS) && return nothing
+    # if there's nothing left, clean up and finish
+    isempty(PAGE_TAGS) && return clean_tags()
 
-    # Get the dictionary tag -> [rp1, rp2...]
+    # Here we have a non-empty PAGE_TAGS dictionary where the keys are
+    # paths of existing files, and values are the tags associated with
+    # these files.
+    # Get the inverse dictionary {tag -> [rp1, rp2...]}
     TAG_PAGES = invert_dict(PAGE_TAGS)
     # store it in globvar
     set_var!(GLOBAL_VARS, "fd_tag_pages", TAG_PAGES)
     all_tags = collect(keys(TAG_PAGES))
 
-    if !isdir(path(:tag))
-        mkpath(path(:tag))
-    else
-        # there may be tags in `refresh_tags` that are not in all_tags
-        # -> in that case delete those pages
-        rm_tags = filter(t -> t ∉ all_tags, refresh_tags)
-        # there may also be existing dirs which shouldn't be there anymore
-        # because the tags have been deleted, these two things combine but
-        # can independently happen (e.g. on the main generate_tag_pages)
-        for dirname in union(setdiff(readdir(path(:tag)), all_tags), rm_tags)
-            rm(joinpath(path(:tag), dirname), recursive=true)
-        end
-    end
+    # some tags may have been given to refresh which don't have any
+    # pages linking to them anymore, these tags will have to be cleaned up
+    rm_tags = filter(t -> t ∉ all_tags, refresh_tags)
 
-    # check which tags should be refreshed, note that
-    update_tags = isempty(refresh_tags) ?
-                    all_tags :
+    # check which tags should be refreshed
+    update_tags = isempty(refresh_tags) ? all_tags :
                     filter(t -> t ∈ all_tags, refresh_tags)
-    write_tag_pages(update_tags)
+
+    # Generate the tag folder if it doesn't exist already
+    isdir(path(:tag)) || mkpath(path(:tag))
+    # Generate the tag layout page if it doesn't exist (it should...)
+    isfile(joinpath(path(:layout), "tag.html")) || write_default_tag_layout()
+    # write each tag, note that they are necessarily in TAG_PAGES
+    for tag in update_tags
+        write_tag_page(tag)
+    end
+    return clean_tags(rm_tags)
+end
+
+"""
+    clean_tags(rm_tags=nothing)
+
+Check the content of the tag folder and remove orphan pages.
+"""
+function clean_tags(rm_tags=nothing)::Nothing
+    isdir(path(:tag)) || return nothing
+    PAGE_TAGS = globvar("fd_page_tags")
+    if isnothing(PAGE_TAGS) || isempty(PAGE_TAGS)
+        all_tags = []
+    else
+        all_tags = union(values(PAGE_TAGS)...)
+    end
+    # check what folders are present
+    all_tag_folders = readdir(path(:tag))
+    # check what folders shouldn't be present
+    orphans = setdiff(all_tags, all_tag_folders)
+    if !isnothing(rm_tags)
+        orphans = union(orphans, rm_tags)
+    end
+    # clean up
+    for dirname in orphans
+        dirpath = joinpath(path(:tag), dirname)
+        isdir(dirpath) && rm(dirpath, recursive=true)
+    end
     return nothing
 end
 
@@ -50,27 +82,49 @@ $SIGNATURES
 
 Internal function to (re)write the tag pages corresponding to `update_tags`.
 """
-function write_tag_pages(update_tags)::Nothing
+function write_tag_page(tag)::Nothing
+    # make `fd_tag` available to that page generation
+    # also allows {{istag tagname}} ... {{end}}
+    set_var!(LOCAL_VARS, "fd_tag", tag)
+
     layout_key  = ifelse(FD_ENV[:STRUCTURE] < v"0.2", :src_html, :layout)
     layout      = path(layout_key)
-    head        = read(joinpath(layout, "head.html"),      String)
-    pg_foot     = read(joinpath(layout, "page_foot.html"), String)
-    foot        = read(joinpath(layout, "foot.html"),      String)
+    content     = read(joinpath(layout, "tag.html"), String)
 
-    # XXX this needs to be fixed, basically tag pages should have some
-    # form of page variable scope as they use the `head` and `foot` which
-    # may use some `{{ ...  }}`. At the moment the tag page uses the scope
-    # of the last-processed page (ambient LOCAL_VARS) which is not good.
+    dir = joinpath(path(:tag), tag)
+    isdir(dir) || mkdir(dir)
 
-    for tag in update_tags
-        # check if `tag/$tag` exists otherwise create it
-        dir = joinpath(path(:tag), tag)
-        isdir(dir) || mkdir(dir)
-        # assemble the page using the hfun list (which can be overwritten
-        # by the user)
-        page = build_page(head, "{{list $tag}}", pg_foot, foot)
-        # write the processed page.
-        write(joinpath(dir, "index.html"), convert_html(page))
-    end
+    write(joinpath(dir, "index.html"), convert_html(content))
+
+    # reset `fd_tag`
+    set_var!(LOCAL_VARS, "fd_tag", "")
+    return nothing
+end
+
+
+"""
+    write_default_tag_layout()
+
+If `_layout/tag.html` is not defined, input a basic default one indicating
+that the user has to modify it. This will help users transition when they may
+have used a template that did not define `_layout/tag.html`.
+"""
+function write_default_tag_layout()::Nothing
+    html = """
+        <!doctype html>
+        <html lang="en">
+        <head>
+          <meta charset="UTF-8">
+          <meta name="viewport" content="width=device-width, initial-scale=1">
+          <title>Tag: {{fill fd_tag}}</title>
+        </head>
+        <body>
+          <div class="{{div_content}} tagpage">
+            {{taglist}}
+          </div>
+        </body>
+        </html>
+        """
+    write(joinpath(path(:layout), "tag.html"), html)
     return nothing
 end
diff --git a/src/eval/module.jl b/src/eval/module.jl
index c4944f68..850a142a 100644
--- a/src/eval/module.jl
+++ b/src/eval/module.jl
@@ -37,7 +37,8 @@ function newmodule(name::String)::Module
             mod = Core.eval(Main, Meta.parse("""
                 module $name
                     import Franklin
-                    import Franklin: @OUTPUT, fdplotly, locvar, pagevar
+                    import Franklin: @OUTPUT, fdplotly, locvar,
+                                     pagevar, globvar, fd2html, get_url
                     if isdefined(Main, :Utils) && typeof(Main.Utils) == Module
                         import ..Utils
                     end
diff --git a/src/manager/dir_utils.jl b/src/manager/dir_utils.jl
index d08ab1dc..884921ac 100644
--- a/src/manager/dir_utils.jl
+++ b/src/manager/dir_utils.jl
@@ -102,7 +102,7 @@ Update the dictionaries referring to input files and their time of last change.
 The variable `verb` propagates verbosity.
 """
 function scan_input_dir!(args...; kw...)
-    to_ignore = vcat(IGNORE_FILES, globvar("ignore"))
+    to_ignore = union(IGNORE_FILES, globvar("ignore"))
     # differentiate between files and dirs
     dir_indicator = [endswith(c, "/") for c in to_ignore]
     d2i = [d for d in to_ignore[dir_indicator] if length(d) > 1]
diff --git a/src/manager/franklin.jl b/src/manager/franklin.jl
index d2835dcd..d927ea13 100644
--- a/src/manager/franklin.jl
+++ b/src/manager/franklin.jl
@@ -259,8 +259,8 @@ function fd_fullpass(watched_files::NamedTuple; clear::Bool=false,
     # generate RSS if appropriate
     globvar("generate_rss") && rss_generator()
 
-    # XXX # generate tags if appropriate
-    # XXX generate_tag_pages()
+    # generate tags if appropriate
+    generate_tag_pages()
 
     # done
     FD_ENV[:FULL_PASS] = false
diff --git a/src/parser/html/tokens.jl b/src/parser/html/tokens.jl
index 35593f73..4cbbbbf8 100644
--- a/src/parser/html/tokens.jl
+++ b/src/parser/html/tokens.jl
@@ -15,13 +15,16 @@ possibilities to consider in which case the order is important: the first case
 that works will be taken.
 """
 const HTML_TOKENS = LittleDict{Char, Vector{TokenFinder}}(
-    '<' => [ isexactly("<!--") => :COMMENT_OPEN  ],  # <!-- ...
+    '<' => [ isexactly("<!--")      => :COMMENT_OPEN, # <!-- ...
+             isexactly("<script>")  => :SCRIPT_OPEN,
+             isexactly("<script ")  => :SCRIPT_OPEN,
+             isexactly("</script>") => :SCRIPT_CLOSE,
+            ],
     '-' => [ isexactly("-->")  => :COMMENT_CLOSE ],  #      ... -->
     '{' => [ isexactly("{{")   => :H_BLOCK_OPEN  ],  # {{
     '}' => [ isexactly("}}")   => :H_BLOCK_CLOSE ],  # }}
     ) # end dict
 
-
 """
 HTML_OCB
 
@@ -31,6 +34,7 @@ const HTML_OCB = [
     # name        opening token    closing token(s)     nestable
     # ----------------------------------------------------------
     OCProto(:COMMENT, :COMMENT_OPEN, (:COMMENT_CLOSE,), false),
+    OCProto(:SCRIPT,  :SCRIPT_OPEN,  (:SCRIPT_CLOSE,),  false),
     OCProto(:H_BLOCK, :H_BLOCK_OPEN, (:H_BLOCK_CLOSE,), true)
     ]
 
@@ -142,7 +146,6 @@ struct HIsPage <: AbstractBlock
     pages::Vector{<:AS} # one or several pages
 end
 
-
 """
 $(TYPEDEF)
 
diff --git a/src/parser/markdown/tokens.jl b/src/parser/markdown/tokens.jl
index 492ed384..af811914 100644
--- a/src/parser/markdown/tokens.jl
+++ b/src/parser/markdown/tokens.jl
@@ -1,5 +1,5 @@
 """
-MD_1C_TOKENS
+    MD_1C_TOKENS
 
 Dictionary of single-char tokens for Markdown. Note that these characters are
 exclusive, they cannot appear again in a larger token.
@@ -12,7 +12,7 @@ const MD_1C_TOKENS = LittleDict{Char, Symbol}(
 
 
 """
-MD_TOKENS_LX
+    MD_TOKENS_LX
 
 Subset of `MD_1C_TOKENS` with only the latex tokens (for parsing what's in a math environment).
 """
@@ -22,7 +22,7 @@ const MD_1C_TOKENS_LX = LittleDict{Char, Symbol}(
     )
 
 """
-MD_TOKENS
+    MD_TOKENS
 
 Dictionary of tokens for Markdown. Note that for each, there may be several
 possibilities to consider in which case the order is important: the first case
@@ -100,7 +100,7 @@ marking it as a potential open brace, same for the close brace.
 
 
 """
-MD_TOKENS_LX
+    MD_TOKENS_LX
 
 Subset of `MD_TOKENS` with only the latex tokens (for parsing what's in a math environment).
 """
@@ -113,7 +113,7 @@ const MD_TOKENS_LX = Dict{Char, Vector{TokenFinder}}(
 
 
 """
-L_RETURNS
+    L_RETURNS
 
 Convenience tuple containing the name for standard line returns and line
 returns followed by an indentation (either a quadruple space or a tab).
@@ -122,7 +122,7 @@ const L_RETURNS = (:LINE_RETURN, :LR_INDENT, :EOS)
 
 
 """
-MD_OCB
+    MD_OCB
 
 List of Open-Close Blocks whose content should be deactivated (any token within
 their span should be marked as inactive) until further processing.
@@ -172,7 +172,7 @@ instance in the case where a user defines a latex command like so:
 
 
 """
-MD_HEADER
+    MD_HEADER
 
 All header symbols.
 """
@@ -180,7 +180,7 @@ const MD_HEADER = (:H1, :H2, :H3, :H4, :H5, :H6)
 
 
 """
-MD_HEADER_OPEN
+    MD_HEADER_OPEN
 
 All header symbols (opening).
 """
@@ -188,7 +188,7 @@ const MD_HEADER_OPEN = (:H1_OPEN, :H2_OPEN, :H3_OPEN, :H4_OPEN, :H5_OPEN, :H6_OP
 
 
 """
-MD_OCB_ESC
+    MD_OCB_ESC
 
 Blocks that will be escaped (their content will not be further processed).
 Corresponds to the "non-reprocess" elements of `MD_OCB`.
@@ -197,7 +197,7 @@ const MD_OCB_ESC = [e.name for e ∈ MD_OCB if !e.nest]
 
 
 """
-MD_OCB_MATH
+    MD_OCB_MATH
 
 Same concept as `MD_OCB` but for math blocks, they can't be nested. Separating
 them from the other dictionary makes their processing easier.
@@ -215,7 +215,7 @@ const MD_OCB_MATH = [
 
 
 """
-MD_OCB_ALL
+    MD_OCB_ALL
 
 Combination of all `MD_OCB` in order.
 """
@@ -223,15 +223,15 @@ const MD_OCB_ALL = vcat(MD_OCB, MD_OCB2, MD_OCB_MATH) # order matters
 
 
 """
-MD_OCB_IGNORE
+    MD_OCB_IGNORE
 
 List of names of blocks that will need to be dropped at compile time.
 """
-const MD_OCB_IGNORE = [:COMMENT, :MD_DEF]
+const MD_OCB_IGNORE = (:COMMENT, :MD_DEF)
 
 
 """
-MATH_BLOCKS_NAMES
+    MATH_BLOCKS_NAMES
 
 List of names of maths environments.
 """
@@ -242,10 +242,10 @@ CODE_BLOCKS_NAMES
 
 List of names of code blocks environments.
 """
-const CODE_BLOCKS_NAMES = [:CODE_BLOCK_LANG, :CODE_BLOCK, :CODE_BLOCK_IND]
+const CODE_BLOCKS_NAMES = (:CODE_BLOCK_LANG, :CODE_BLOCK, :CODE_BLOCK_IND)
 
 """
-MD_OCB_NO_INNER
+    MD_OCB_NO_INNER
 
 List of names of blocks which will deactivate any block contained within them
 as their content will be reprocessed later on.
diff --git a/src/parser/markdown/validate.jl b/src/parser/markdown/validate.jl
index 0b179402..a41f81ee 100644
--- a/src/parser/markdown/validate.jl
+++ b/src/parser/markdown/validate.jl
@@ -166,7 +166,8 @@ function find_double_brace_blocks(tokens)
                                    context(thead)))
             end
             hnext += head
-            push!(dbb, OCBlock(:DOUBLE_BRACE, thead => db_tokens[hnext]))
+            ocb = OCBlock(:DOUBLE_BRACE, thead => db_tokens[hnext])
+            push!(dbb, ocb)
             head = hnext
         end
         head += 1
diff --git a/src/parser/ocblocks.jl b/src/parser/ocblocks.jl
index bc4ab4d3..df33e3e5 100644
--- a/src/parser/ocblocks.jl
+++ b/src/parser/ocblocks.jl
@@ -94,7 +94,9 @@ reprocessed. There's effectively two cases:
 2. (solved in `check_and_merge_indented_blocks!`) when an indented code block
     is contained inside a larger block.
 """
-function deactivate_inner_blocks!(blocks::Vector{OCBlock})
+function deactivate_inner_blocks!(blocks::Vector{OCBlock}, nin=MD_OCB_NO_INNER)
+    ranges  = Vector{Pair{Int,Int}}()
+    isempty(blocks) && return ranges
     # see #444 it's important to ensure the blocks are sorted and they now
     # may not be given that we're finding them in 2 passes.
     sort!(blocks, by=(β->from(β)))
@@ -107,10 +109,11 @@ function deactivate_inner_blocks!(blocks::Vector{OCBlock})
     heads   = from.(blocks)
     while i < nb
         cblock = blocks[i]
-        if active[i] && cblock.name ∈ MD_OCB_NO_INNER
+        if active[i] && cblock.name ∈ nin
             # find all blocks within the span of this block, deactivate them
             chead = heads[i]
             ctail = to(cblock)
+            push!(ranges, (chead => ctail))
             # look at all blocks starting after the current one that may
             # be within its span (note that, at worst, we have a few thousand
             # blocks here so that it doesn't need to be super optimised...)
@@ -120,10 +123,29 @@ function deactivate_inner_blocks!(blocks::Vector{OCBlock})
         i += 1
     end
     deleteat!(blocks, map(!, active))
-    return nothing
+    return ranges
 end
 
 
+"""
+$SIGNATURES
+
+Given ranges found by `deactivate_inner_blocks!` deactivate double brace blocks
+that are within those ranges (they will be reprocessed later).
+"""
+function deactivate_inner_dbb!(dbb, ranges)
+    isempty(dbb) && return nothing
+    isempty(ranges) && return nothing
+    active = ones(Bool, length(dbb))
+    for (i, d) in enumerate(dbb)
+        hd, td = from(d), to(d)
+        r = findfirst(r -> r.first <= hd && td <= r.second, ranges)
+        isnothing(r) || (active[i] = false)
+    end
+    deleteat!(dbb, map(!, active))
+    return nothing
+end
+
 
 """
 $(SIGNATURES)
diff --git a/src/parser/tokens.jl b/src/parser/tokens.jl
index 0eafec0c..c304031d 100644
--- a/src/parser/tokens.jl
+++ b/src/parser/tokens.jl
@@ -235,7 +235,7 @@ In combination with `incrlook`, checks to see if we have something that looks
 like a @@div describing the opening of a div block. Triggering char is a first
 `@`.
 """
-is_div_open(i::Int, c::Char) = (i == 1 && return c == '@'; return α(c, ('-','_', NUM_CHAR...)))
+is_div_open(i::Int, c::Char) = (i == 1 && return c == '@'; return α(c, ('-','_', ',', NUM_CHAR...)))
 
 """
 $(SIGNATURES)
diff --git a/src/utils/html.jl b/src/utils/html.jl
index 8673f8a6..a861fc9d 100644
--- a/src/utils/html.jl
+++ b/src/utils/html.jl
@@ -125,6 +125,33 @@ function _url_curpage2()
     return fn
 end
 
+"""
+    get_url
+
+Take a `rpath` and return the corresponding valid url
+"""
+function get_url(rpath)
+    if FD_ENV[:STRUCTURE] < v"0.2"
+        error("`get_url` undefined for older file specs, open an issue on Github.")
+    end
+    rpc, ext = splitext(rpath)
+    if ext in (".md", ".html")
+        url = rpc
+    else
+        url = rpath
+    end
+    if endswith(url, "index")
+        url = url[1:length(url)-length("index")]
+    end
+    url = strip(url, '/')
+    if isempty(url)
+        url = "/"
+    else
+        url = "/$url/"
+    end
+    return url
+end
+
 # Copied from https://github.com/JuliaLang/julia/blob/acb7bd93fb2d5adbbabeaed9f39ab3c85495b02f/stdlib/Markdown/src/render/html.jl#L25-L31
 const _htmlescape_chars = LittleDict(
             '<' => "&lt;",
diff --git a/src/utils/paths.jl b/src/utils/paths.jl
index ceadb9ca..269508ab 100644
--- a/src/utils/paths.jl
+++ b/src/utils/paths.jl
@@ -11,7 +11,8 @@ IGNORE_FILES
 
 Collection of file names that will be ignored at compile time.
 """
-const IGNORE_FILES = [".DS_Store", ".gitignore", "LICENSE.md", "README.md"]
+const IGNORE_FILES = [".DS_Store", ".gitignore", "LICENSE.md", "README.md",
+                      "franklin", "franklin.pub", "node_modules/"]
 
 
 """
diff --git a/src/utils/vars.jl b/src/utils/vars.jl
index 50d3480b..1f9bdb11 100644
--- a/src/utils/vars.jl
+++ b/src/utils/vars.jl
@@ -90,7 +90,8 @@ const LOCAL_VARS_DEFAULT = [
     "fd_ctime"  => Pair(Date(1),    (Date,)),   # time of creation
     "fd_mtime"  => Pair(Date(1),    (Date,)),   # time of last modification
     "fd_rpath"  => Pair("",         (String,)), # rpath to current page
-    "fd_url"    => Pair("",         (String,)),
+    "fd_url"    => Pair("",         (String,)), # url to current page
+    "fd_tag"    => Pair("",         (String,)), # (generated) current tag
     ]
 #=
 NOTE:
@@ -194,12 +195,17 @@ function pagevar(rpath::AS, name::Union{Symbol,String})
         set_cur_rpath(fpath, isrelative=true)
         # effectively we only care about the mddefs
         convert_md(read(fpath, String), pagevar=true)
-        # re-set the cur path to what it was before
-        set_cur_rpath(bk_path, isrelative=true)
-        # re-set local vars using ALL_PAGE_VARS
-        # NOTE: we must do this in place to messing things up.
-        empty!(LOCAL_VARS)
-        merge!(LOCAL_VARS, ALL_PAGE_VARS[bk_path_])
+
+        if !haskey(ALL_PAGE_VARS, bk_path_) # empty corner case in tests
+            def_LOCAL_VARS!()
+        else
+            # re-set the cur path to what it was before
+            set_cur_rpath(bk_path, isrelative=true)
+            # re-set local vars using ALL_PAGE_VARS
+            # NOTE: we must do this in place to messing things up.
+            empty!(LOCAL_VARS)
+            merge!(LOCAL_VARS, ALL_PAGE_VARS[bk_path_])
+        end
     end
     name = String(name)
     haskey(ALL_PAGE_VARS[rpath], name) || return nothing


Test patch:
diff --git a/test/converter/html/html2.jl b/test/converter/html/html2.jl
index 9685b034..90f48cef 100644
--- a/test/converter/html/html2.jl
+++ b/test/converter/html/html2.jl
@@ -51,23 +51,3 @@ end
     fhs = F.process_html_qblocks(hs, qblocks)
     @test isapproxstr(fhs, "AEF")
 end
-
-
-@testset "Bad cases" begin
-    F.def_LOCAL_VARS!()
-    # Lonely End block
-    s = """A {{end}}"""
-    @test_throws F.HTMLBlockError F.convert_html(s)
-
-    # Inbalanced
-    s = """A {{if a}} B {{if b}} C {{else}} {{end}}"""
-    @test_throws F.HTMLBlockError F.convert_html(s)
-
-    # Some of the conditions are not bools
-    F.set_vars!(F.LOCAL_VARS, [
-        "a" => "false",
-        "b" => "false",
-        "c" => "\"Hello\""])
-    s = """A {{if a}} A {{elseif c}} B {{end}}"""
-    @test_throws F.HTMLBlockError F.convert_html(s)
-end
diff --git a/test/converter/md/tags.jl b/test/converter/md/tags.jl
index 190b42dc..848b510e 100644
--- a/test/converter/md/tags.jl
+++ b/test/converter/md/tags.jl
@@ -9,8 +9,8 @@
         """)
     F.def_GLOBAL_VARS!()
     F.def_LOCAL_VARS!()
-    write("pg1.md", "")
-    write("pg2.md", "")
+    write("pg1.md", "@def title = \"Page1\"")
+    write("pg2.md", "@def title = \"Page2\"")
     F.set_var!(F.GLOBAL_VARS, "fd_page_tags", F.DTAG(("pg1" => Set(["aa", "bb"]),)))
     F.globvar("fd_page_tags")["pg2"] = Set(["bb", "cc"])
 
@@ -28,6 +28,12 @@
     @test isfile(joinpath(F.path(:tag), "bb", "index.html"))
     @test isfile(joinpath(F.path(:tag), "cc", "index.html"))
 
+    tagbb = read(joinpath(F.path(:tag), "bb", "index.html"), String)
+    tagcc = read(joinpath(F.path(:tag), "cc", "index.html"), String)
+
+    @test occursin("""<ul><li><a href="/pg1/">Page1</a></li><li><a href="/pg2/">Page2</a></li></ul>""", tagbb)
+    @test occursin("""<ul><li><a href="/pg2/">Page2</a></li></ul>""", tagcc)
+
     F.clear_dicts()
 end
 
@@ -76,15 +82,10 @@ end
 
     p = joinpath("__site", "tag", "aa", "index.html")
     c = read(p, String)
-    @test isapproxstr(c, """
-        <div class="franklin-content">
-        <h1>Tag: aa</h1>
-        <ul>
-          <li><a href="/blog/pg4/">Page 4</a></li>
-          <li><a href="/blog/pg1/">Page 1</a></li>
-        </ul>
-        </div>
-        """)
+    @test occursin("""Tag: aa""", c)
+    @test occursin("""<div class=\"franklin-content tagpage\">""", c)
+    @test occursin("""<ul><li><a href="/blog/pg4/">Page 4</a></li>""", c)
+    @test occursin("""<li><a href="/blog/pg1/">Page 1</a></li></ul>""", c)
 
     # Now remove some pages; we use bash commands so that they're blocking
     success(`rm $(joinpath("blog", "pg4.md"))`)
@@ -92,14 +93,8 @@ end
 
     serve(clear=true, single=true, nomess=true)
     c = read(p, String)
-    @test isapproxstr(c, """
-        <div class="franklin-content">
-        <h1>Tag: aa</h1>
-        <ul>
-          <li><a href="/blog/pg1/">Page 1</a></li>
-        </ul>
-        </div>
-        """)
+    @test occursin("""<div class=\"franklin-content tagpage\">""", c)
+    @test occursin("""<ul><li><a href=\"/blog/pg1/\">Page 1</a></li></ul>""", c)
 
     F.clear_dicts()
 end
diff --git a/test/global/cases2.jl b/test/global/cases2.jl
index ee1cefd7..b1ef339e 100644
--- a/test/global/cases2.jl
+++ b/test/global/cases2.jl
@@ -187,7 +187,7 @@ end
         {{hasmath}} {{hascode}}
         $x = 5$
         """ |> fdi
-    @test isapproxstr(s, "true false \\(x = 5\\)")
+    @test isapproxstr(s, "<p>true false \\(x = 5\\)</p>")
     F.set_var!(F.LOCAL_VARS, "hascode", false)
     F.set_var!(F.LOCAL_VARS, "hasmath", false)
     s = raw"""
@@ -196,8 +196,8 @@ end
         blah
         ```
         """ |> fdi
-    @test isapproxstr(s, """
+    @test isapproxstr(s, """<p>
         false true
-        <pre><code class=\"language-r\">blah</code></pre>
+        <pre><code class=\"language-r\">blah</code></pre></p>
         """)
 end
diff --git a/test/parser/markdown-extra.jl b/test/parser/markdown-extra.jl
index 554e9e2b..fd09d487 100644
--- a/test/parser/markdown-extra.jl
+++ b/test/parser/markdown-extra.jl
@@ -117,13 +117,13 @@ end
         @def title = "hello"
         {{title}}{{title}}
         """ |> fd2html_td
-    @test isapproxstr(s, "hellohello")
+    @test isapproxstr(s, "<p>hellohello</p>")
     s = """
         @def a_b = "hello"
         @def c_d = "goodbye"
         {{a_b}}{{c_d}}
         """ |> fd2html_td
-    @test isapproxstr(s, "hellogoodbye")
+    @test isapproxstr(s, "<p>hellogoodbye</p>")
 end
 
 # issue 424 with double braces
@@ -134,7 +134,7 @@ end
         $\rho=\frac{e^{-\beta \mathcal{E}_{s}}} {\mathcal{Z}} $
         """ |> fd2html_td
     @test isapproxstr(s, """
-        hello \\(\\rho=\\frac{e^{-\\beta \\mathcal{E}_{s}}} {\\mathcal{Z}} \\)""")
+        <p>hello \\(\\rho=\\frac{e^{-\\beta \\mathcal{E}_{s}}} {\\mathcal{Z}} \\)</p>""")
 end
 
 # issue 432 and consequences
diff --git a/test/parser/md-dbb.jl b/test/parser/md-dbb.jl
new file mode 100644
index 00000000..f8f8c514
--- /dev/null
+++ b/test/parser/md-dbb.jl
@@ -0,0 +1,53 @@
+@testset "Bad cases" begin
+    F.def_LOCAL_VARS!()
+    # Lonely End block
+    s = """A {{end}}"""
+    @test_throws F.HTMLBlockError F.convert_html(s)
+
+    # Inbalanced
+    s = """A {{if a}} B {{if b}} C {{else}} {{end}}"""
+    @test_throws F.HTMLBlockError F.convert_html(s)
+
+    # Some of the conditions are not bools
+    F.set_vars!(F.LOCAL_VARS, [
+        "a" => "false",
+        "b" => "false",
+        "c" => "\"Hello\""])
+    s = """A {{if a}} A {{elseif c}} B {{end}}"""
+    @test_throws F.HTMLBlockError F.convert_html(s)
+end
+
+
+@testset "Script" begin
+    F.def_LOCAL_VARS!()
+    F.set_var!(F.LOCAL_VARS, "hasmath", true)
+    s = """
+        Hasmath: {{hasmath}}
+        <script>{{hasmath}}</script>
+        <script src="...">{{hasmath}}</script>
+        """
+    @test isapproxstr(F.convert_html(s), """
+        Hasmath: true
+        <script>{{hasmath}}</script>
+        <script src="...">{{hasmath}}</script>
+        """)
+end
+
+# issue #482
+@testset "div-dbb" begin
+    Franklin.eval(:(hfun_bar(p) = string(round(sqrt(Meta.parse(p[1])), digits=1)) ))
+    s = "@@B @@A {{author}} @@\n@@ <!-- html -->\n" |> fd2html
+    @test isapproxstr(s, """
+        <div class=\"B\"><div class=\"A\">THE AUTHOR</div></div>
+        """)
+    s = "**{{author}}**" |> fd2html
+    @test isapproxstr(s, "<p><strong>THE AUTHOR</strong></p>")
+    s = raw"\style{font-weight:bold;}{ {{author}} }" |> fd2html
+    @test isapproxstr(s, """
+        <span style="font-weight:bold;">THE AUTHOR</span>
+        """)
+    s = raw"@@bold {{bar 4}} @@" |> fd2html
+    @test isapproxstr(s, """
+        <div class="bold">2.0</div>
+        """)
+end
diff --git a/test/runtests.jl b/test/runtests.jl
index 708a9f67..f4e9e516 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -39,6 +39,7 @@ include("parser/markdown-extra.jl")
 include("parser/footnotes+links.jl")
 include("parser/latex++.jl")
 include("parser/indentation++.jl")
+include("parser/md-dbb.jl")
 println("🍺")
 
 # EVAL
@@ -61,7 +62,7 @@ include("converter/md/markdown3.jl")
 include("converter/md/markdown4.jl")
 include("converter/md/hyperref.jl")
 include("converter/md/md_defs.jl")
-# include("converter/md/tags.jl")
+include("converter/md/tags.jl")
 println("🍺")
 println("CONVERTER/HTML")
 include("converter/html/html.jl")
@@ -104,7 +105,7 @@ cd(dirname(dirname(pathof(Franklin))))
 
 println("TEMPLATING")
 include("templating/for.jl")
-include("templating/fill.jl") # XXX --> (path issue)
+include("templating/fill.jl")
 
 println("UTILS FILE")
 include("utils_file/basic.jl")
diff --git a/test/utils/misc.jl b/test/utils/misc.jl
index ca76b638..0801f17b 100644
--- a/test/utils/misc.jl
+++ b/test/utils/misc.jl
@@ -202,3 +202,13 @@ end
     @test Set(id["aa"]) == Set(["blog/pg1", "blog/pg3"])
     @test Set(id["ee"]) == Set(["blog/pg3"])
 end
+
+@testset "get_url" begin
+    fs2()
+    @test get_url("foo") == "/foo/"
+    @test get_url("foo/bar") == "/foo/bar/"
+    @test get_url("index") == "/"
+    @test get_url("foo/index") == "/foo/"
+    @test get_url("foo.md") == "/foo/"
+    @test get_url("foo/index.md") == "/foo/"
+end