Repository: tlienart/Franklin.jl
Pull request #603 (2020-08-31 14:23:56)

Better warning messages
Adds significantly improved warning messages when

- there's a problem with a  page var definition
- there's an issue with RSS generation
- there's an issue with running code
- there's an issue with using a page var

and a few other such things that were causing often unhelpful warnings.

Addresses #544 

Note: can be globally turned off passing the keyword `show_warnings=false` to `serve`. These are not Julia warning and so will not be affected by logging  levels (and vice versa: using `show_warnings=false` will  only  suppress the  Franklin-warnings


Patch:
diff --git a/Project.toml b/Project.toml
index aea1e969..8c25feaf 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,10 +1,9 @@
 name = "Franklin"
 uuid = "713c75ef-9fc9-4b05-94a9-213340da978e"
 authors = ["Thibaut Lienart <tlienart@me.com>"]
-version = "0.9.10"
+version = "0.9.11"
 
 [deps]
-Crayons = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f"
 Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
 DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab"
 DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
@@ -21,7 +20,6 @@ REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
 Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
 
 [compat]
-Crayons = "4"
 DocStringExtensions = "0.8"
 FranklinTemplates = "0.6,0.7"
 HTTP = "0.8"
@@ -36,7 +34,8 @@ DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
 DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
 LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
 Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
+Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb"
 Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
 
 [targets]
-test = ["Test", "DataStructures", "DataFrames", "Random", "LinearAlgebra"]
+test = ["Test", "DataStructures", "DataFrames", "Random", "LinearAlgebra", "Suppressor"]
diff --git a/src/Franklin.jl b/src/Franklin.jl
index 58694fed..102a3344 100644
--- a/src/Franklin.jl
+++ b/src/Franklin.jl
@@ -10,7 +10,6 @@ using DelimitedFiles: readdlm
 using OrderedCollections
 using Pkg
 using DocStringExtensions: SIGNATURES, TYPEDEF
-using Crayons
 
 import Logging
 import LiveServer
@@ -39,6 +38,17 @@ export jd2html # = fd2html
 # CONSTANTS
 #
 
+const HOME =
+    "https://franklinjl.org"
+const POINTER_PV =
+    "- page variables and {{...}} blocks: $HOME/syntax/page-variables/"
+const POINTER_HFUN =
+    "- h-functions (hfun) and lx-functions: $HOME/syntax/utils/"
+const POINTER_EVAL =
+    "- evaluation of Julia code blocks: $HOME/code/"
+const POINTER_WORKFLOW =
+    "- workflow and folder structure: $HOME/workflow/"
+
 # These are common IP addresses that we can quickly ping to see if the
 # user seems online. This is used in `verify_links`. The IPs were
 # obtained via `dig www...`; they may change over time; see `check_ping`
@@ -59,9 +69,12 @@ const FD_ENV = LittleDict(
     :SUPPRESS_ERR  => false,
     :SILENT_MODE   => false,
     :OFFSET_LXDEFS => -BIG_INT,
-    :CUR_PATH      => "",
+    :CUR_PATH      => "",       # complements fd-rpath
+    :SOURCE        => "",       # keeps track of the origin of a HTML string
     :STRUCTURE     => v"0.2",
-    :QUIET_TEST    => false,)
+    :QUIET_TEST    => false,
+    :SHOW_WARNINGS => true,
+    )
 
 """Dict to keep track of languages and how comments are indicated and their extensions. This is relevant to allow hiding lines of code. """
 const CODE_LANG = LittleDict{String,NTuple{2,String}}(
@@ -113,9 +126,9 @@ const LOGGING = Ref(false)
 
 function logger(t)
     LOGGING[] || return nothing
-    print(Crayon(bold=true, foreground=:yellow), "LOG: ")
-    print(Crayon(bold=false, foreground=:blue), rpad(t[1], 20))
-    println(Crayon(reset=true), escape_string(t[2]))
+    printstyled("LOG: ", color=:yellow, bold=true)
+    printstyled(rpad(t[1], 20), color=:blue)
+    println(escape_string(t[2]))
     return nothing
 end
 
@@ -126,11 +139,13 @@ include("build.jl") # check if user has Node/minify
 include("regexes.jl")
 
 # UTILS
+include("utils/warnings.jl")
+include("utils/errors.jl")
 include("utils/paths.jl")
 include("utils/vars.jl")
 include("utils/misc.jl")
 include("utils/html.jl")
-include("utils/errors.jl")
+
 
 # PARSING
 include("parser/tokens.jl")
diff --git a/src/converter/html/functions.jl b/src/converter/html/functions.jl
index 26214be4..8f4300f0 100644
--- a/src/converter/html/functions.jl
+++ b/src/converter/html/functions.jl
@@ -20,8 +20,14 @@ function convert_html_fblock(β::HFun)::String
         return hfun_fill([β.fname])
     end
     # if we get here, then the function name is unknown, warn and ignore
-    @warn "I found a function block '{{$(β.fname) ...}}' but I don't " *
-          "recognise the function name. Ignoring."
+    print_warning("""
+        A block '{{$(β.fname) ...}}' was found but the name '$(β.fname)' does
+        not correspond to a built-in block or hfun nor does it match anything
+        defined in 'utils.jl'. It might have been misspelled.
+        \nRelevant pointers:
+        $POINTER_PV
+        $POINTER_HFUN
+        """)
     # returning empty
     return ""
 end
@@ -49,8 +55,7 @@ function hfun_fill(params::Vector{String})::String
         else
             tmp_repl = locvar(vname)
             if isnothing(tmp_repl)
-                @warn "I found a '{{fill $vname}}' but I do not know the " *
-                      "variable '$vname'. Ignoring."
+                hfun_unknown_arg1_warn(:fill, vname)
             else
                 repl = string(tmp_repl)
             end
@@ -59,8 +64,13 @@ function hfun_fill(params::Vector{String})::String
         rpath = params[2]
         tmp_repl = pagevar(rpath, vname)
         if isnothing(tmp_repl)
-            @warn "I found a '{{fill $vname $rpath}}' but I do not know the " *
-                  "variable '$vname' or the path '$rpath'. Ignoring."
+            hfun_misc_warn(:fill, """
+                The arguments '$vname' and '$rpath' cannot be resolved. It may
+                be that the path doesn't exist or that the variable '$vname'
+                is not defined on the page at that path.
+                \nRelevant pointers:
+                $POINTER_PV
+                """)
         else
             repl = string(tmp_repl)
         end
@@ -89,7 +99,9 @@ function hfun_insert(params::Vector{String})::String
     if isfile(fpath)
         repl = convert_html(read(fpath, String))
     else
-        @warn "I found an {{insert ...}} block and tried to insert '$fpath' but I couldn't find the file. Ignoring."
+        hfun_misc_warn(:insert, """
+            Couldn't find the file '$fpath' to resolve the insertion.
+            """)
     end
     return repl
 end
@@ -116,7 +128,9 @@ function hfun_href(params::Vector{String})::String
         haskey(PAGE_BIBREFS, hkey) || return repl
         repl = html_ahref_key(hkey, PAGE_BIBREFS[hkey])
     else
-        @warn "Unknown dictionary name $dname in {{href ...}}. Ignoring"
+        hfun_misc_warn(:href, """
+            Unknown reference dictionary '$dname'.
+            """)
     end
     return repl
 end
@@ -282,8 +296,10 @@ function hfun_paginate(params::Vector{String})::String
     end
     iter = locvar(params[1])
     if isnothing(iter)
-        @warn "In a {{paginate ...}} block, I couldn't recognise the " *
-              "name of the iterable. Nothing will get printed as a result."
+        hfun_misc_warn(:paginate, """
+            The page variable '$(params[1])' does not match the name of a page
+            variable. The call will be ignored.            
+            """)
         return ""
     end
     npp = locvar(params[2])
@@ -291,22 +307,26 @@ function hfun_paginate(params::Vector{String})::String
         try
             npp = parse(Int, params[2])
         catch
-            @warn "In a {{paginate ...}} block, I couldn't parse the number " *
-                  "of items per page. Defaulting to 10."
+            hfun_misc_warn(:paginate, """
+                Failed to parse the number of items per page (given: '$(params[2])'). Setting to 10.
+                """)
             npp = 10
         end
     end
     if npp <= 0
-        @warn "In a {{paginate ...}} block, the number of items per page is " *
-              "non-positive, defaulting to 10."
+        hfun_misc_warn(:paginate, """
+            Non-positive number of items per page ('$npp' read from $(params[2])). Setting to 10.
+            """)
         npp = 10
     end
 
     # Was there already a pagination element on this page?
     # if so warn and ignore
     if !isnothing(locvar(:paginate_itr))
-        @warn "It looks like you have multiple calls to {{paginate ...}} on " *
-              "the page; only one is supported. Verify."
+        hfun_misc_warn(:paginate, """
+            Multiple calls to '{{paginate ...}}' on the page but at most one is
+            expected.
+            """)
         return ""
     end
     # we're just storing the name here, so we'll have to locvar(locvar(.))
diff --git a/src/converter/html/html.jl b/src/converter/html/html.jl
index 5fedc3d2..90c81e6f 100644
--- a/src/converter/html/html.jl
+++ b/src/converter/html/html.jl
@@ -47,7 +47,8 @@ page variables. See also [`fd2html`](@ref) which only returns the html.
 """
 function fd2html_v(st::AS; internal::Bool=false,
                    dir::String="", nop::Bool=false)::Tuple{String,Dict}
-    isempty(st) && return st
+    FD_ENV[:SOURCE] = "input string"
+    isempty(st) && return (st, LittleDict())
     if !internal
         empty!(ALL_PAGE_VARS)
         FOLDER_PATH[] = isempty(dir) ? mktempdir() : dir
diff --git a/src/converter/markdown/mddefs.jl b/src/converter/markdown/mddefs.jl
index d347eeef..b2bf6892 100644
--- a/src/converter/markdown/mddefs.jl
+++ b/src/converter/markdown/mddefs.jl
@@ -39,8 +39,7 @@ function process_mddefs(blocks::Vector{OCBlock}, isconfig::Bool,
                 if check_type(typeof(value), acc_types)
                     curdict[key] = Pair(value, acc_types)
                 else
-                    @warn "Page var '$key' (type(s): $acc_types) can't be set " *
-                          "to value '$value' (type: $(typeof(value))). Assignment ignored."
+                    mddef_warn(key, value, acc_types)
                 end
             else
                 set_var!(curdict, key, value)
@@ -57,9 +56,13 @@ function process_mddefs(blocks::Vector{OCBlock}, isconfig::Bool,
         inner = stent(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)."
+            print_warning("""
+                Delimiters for an '@def ...' environement were found but at
+                least one assignment does not have the proper syntax. That
+                assignment will be ignored.
+                \nRelevant pointers:
+                $POINTER_PV
+                """)
             continue
         end
         vname, vdef = m.captures[1:2]
diff --git a/src/converter/markdown/tags.jl b/src/converter/markdown/tags.jl
index 9f2bcfe2..cf365f5f 100644
--- a/src/converter/markdown/tags.jl
+++ b/src/converter/markdown/tags.jl
@@ -83,6 +83,7 @@ $SIGNATURES
 Internal function to (re)write the tag pages corresponding to `update_tags`.
 """
 function write_tag_page(tag)::Nothing
+    FD_ENV[:SOURCE] = "generated - tag"
     # make `fd_tag` available to that page generation
     set_var!(LOCAL_VARS, "fd_tag", tag)
 
diff --git a/src/eval/codeblock.jl b/src/eval/codeblock.jl
index ef53bd66..844c8497 100644
--- a/src/eval/codeblock.jl
+++ b/src/eval/codeblock.jl
@@ -78,7 +78,11 @@ function resolve_code_block(ss::SubString)::String
     isnothing(rpath) && return html_code(code, lang)
     # 1.b if not julia code, eval is not supported
     if lang != "julia"
-        @warn "Evaluation of non-Julia code blocks is not yet supported."
+        print_warning("""
+            Evaluation of non-Julia code blocks is not yet supported.
+            \nRelevant pointers:
+            $POINTER_EVAL
+            """)
         return html_code(code, lang)
     end
 
diff --git a/src/eval/literate.jl b/src/eval/literate.jl
index 5cfeacca..d1e92ee7 100644
--- a/src/eval/literate.jl
+++ b/src/eval/literate.jl
@@ -19,7 +19,9 @@ function literate_to_franklin(rpath::AS)::Tuple{String,Bool}
     # append `.jl` if required
     endswith(fpath, ".jl") || (fpath *= ".jl")
     if !isfile(fpath)
-        @warn "File not found when trying to convert a literate file ($fpath)."
+        print_warning("""
+            File not found when trying to  convert a literate file at '$fpath'.
+            """)
         return "", true
     end
     if FD_ENV[:STRUCTURE] < v"0.2"
diff --git a/src/eval/run.jl b/src/eval/run.jl
index 59b9fd2e..0d6a46f9 100644
--- a/src/eval/run.jl
+++ b/src/eval/run.jl
@@ -78,7 +78,13 @@ function run_code(mod::Module, code::AS, out_path::AS;
         # TODO: add more informative message, maybe show type of error
         # + parent path
         FD_ENV[:SILENT_MODE] || print("\n")
-        warn_err && @warn "There was an error of type $err running the code."
+        warn_err && print_warning("""
+            There was an error of type '$err' when running a code block.
+            Checking the output files '$(splitext(out_path)[1]).(out|res)'
+            might be helpful to understand and solve the issue.
+            \nRelevant pointers:
+            $POINTER_EVAL
+            """)
         res = nothing
     end
     # if last bit ends with `;` return nothing (no display)
diff --git a/src/manager/file_utils.jl b/src/manager/file_utils.jl
index 5f635322..daa0165e 100644
--- a/src/manager/file_utils.jl
+++ b/src/manager/file_utils.jl
@@ -9,6 +9,7 @@ where only structural variables are considered (e.g. controlling folder
 structure).
 """
 function process_config(; init::Bool=false)::Nothing
+    FD_ENV[:SOURCE] = "config.md"
     if init
         # initially the paths variable aren't set, try to find a config.md
         # and read definitions in it; in particular folder_structure.
@@ -19,7 +20,7 @@ function process_config(; init::Bool=false)::Nothing
         elseif isfile(config_path_v1)
             convert_md(read(config_path_v1, String); isconfig=true)
         else
-            @warn "I didn't find a config file. Ignoring."
+            config_warn()
         end
     else
         key = ifelse(FD_ENV[:STRUCTURE] < v"0.2", :src, :folder)
@@ -28,7 +29,7 @@ function process_config(; init::Bool=false)::Nothing
         if isfile(config_path)
             convert_md(read(config_path, String); isconfig=true)
         else
-            @warn "I didn't find a config file. Ignoring."
+            config_warn()
         end
     end
     return nothing
@@ -44,6 +45,7 @@ in particular users can redefine the behaviour of `hfuns` though that's not
 recommended.
 """
 function process_utils()
+    FD_ENV[:SOURCE] = "utils.jl"
     utils_path_v1 = joinpath(FOLDER_PATH[], "src", "utils.jl")
     utils_path_v2 = joinpath(FOLDER_PATH[], "utils.jl")
     if isfile(utils_path_v2)
@@ -109,9 +111,11 @@ function process_file_err(
     inp  = joinpath(fpair...)
     outp = form_output_path(fpair.first, fpair.second, case)
     if case == :md
+        FD_ENV[:SOURCE] = get_rpath(inp)
         convert_and_write(fpair..., head, pgfoot, foot, outp;
                    prerender=prerender, isoptim=isoptim, on_write=on_write)
     elseif case == :html
+        FD_ENV[:SOURCE] = get_rpath(inp)
         set_cur_rpath(joinpath(fpair...))
         set_page_env()
         raw_html  = read(inp, String)
@@ -136,6 +140,7 @@ function process_file_err(
         end
         @label end_copyblock
     end
+    FD_ENV[:SOURCE] = ""
     FD_ENV[:FULL_PASS] || FD_ENV[:SILENT_MODE] || rprint("→ page updated [✓]")
     return nothing
 end
@@ -154,7 +159,7 @@ change_ext(fname::AS, ext=".html")::String = splitext(fname)[1] * ext
 
 Extracts the relative file system path out of the full system path to a file
 currently being processed. Does not start with a path separator.
-So `[some_fs_path]/blog/page.md` --> `blog/page.md`.
+So `[some_fs_path]/blog/page.md` --> `blog/page.md` (keeps the extension).
 """
 function get_rpath(fpath::String)
     root = path(ifelse(FD_ENV[:STRUCTURE] < v"0.2", :src, :folder))
diff --git a/src/manager/franklin.jl b/src/manager/franklin.jl
index e59fd9a9..e989d476 100644
--- a/src/manager/franklin.jl
+++ b/src/manager/franklin.jl
@@ -43,6 +43,7 @@ Keyword arguments:
                       passing as arguments the rendered page and the page
                       variables
 * `host="127.0.0.1"`: the host to use for the local server
+* `show_warnings`:    whether to show franklin  warnings
 """
 function serve(; clear::Bool=false,
                  verb::Bool=false,
@@ -57,7 +58,8 @@ function serve(; clear::Bool=false,
                  cleanup::Bool=true,
                  on_write::Function=(_, _) -> nothing,
                  log::Bool=false,
-                 host::String="127.0.0.1"
+                 host::String="127.0.0.1",
+                 show_warnings::Bool=true
                  )::Union{Nothing,Int}
     LOGGING[] = log
     # set the global path
@@ -65,6 +67,10 @@ function serve(; clear::Bool=false,
     # silent mode?
     silent && (FD_ENV[:SILENT_MODE] = true; verb = false)
 
+    if silent || !show_warnings
+        FD_ENV[:SHOW_WARNINGS] = false
+    end
+
     # in case of optim, there may be a prepath given which should be
     # kept
     prepath = get(GLOBAL_VARS, "prepath", nothing)
@@ -236,8 +242,8 @@ function fd_fullpass(watched_files::NamedTuple; clear::Bool=false,
     hasindexhtml = isfile(joinpath(root, "index.html"))
 
     if !hasindexmd && !hasindexhtml
-        @warn "No /index.md or /index.html found, there should be one. " *
-              "Ignoring..."
+        @warn "No 'index.md' or 'index.html' found in the root directory, " *
+              "there should be one. Ignoring..."
     end
 
     # go over all pages note that the html files are processed AFTER the
diff --git a/src/manager/rss_generator.jl b/src/manager/rss_generator.jl
index 4cbc2412..96915bc8 100644
--- a/src/manager/rss_generator.jl
+++ b/src/manager/rss_generator.jl
@@ -81,8 +81,9 @@ function add_rss_item()::RSSItem
 
     # warning for title which should really be defined
     isnothing(title) && (title = "")
-    isempty(title)   && @warn "Found an RSS description but no " *
-                              "title for page $link."
+    isempty(title)   && print_warning("""
+        An RSS description was found but without title for page '$link'.
+        """)
 
     RSS_DICT[link] = RSSItem(title, link, descr, author,
                              category, comments, enclosure, pubDate)
@@ -105,10 +106,14 @@ function rss_generator()::Nothing
     rss_link  = globvar("website_url")
 
     if any(isempty, (rss_title, rss_descr, rss_link))
-        @warn """I found RSS items but the RSS feed is not properly described:
-              at least one of the following variables has not been defined in
-              your config.md: `website_title`, `website_descr`, `website_url`.
-              The feed will not be (re)generated."""
+        print_warning("""
+            RSS items were found but the RSS feed is improperly described:
+            at least one of the following variables have not been defined in
+            your 'config.md': 'website_title', 'website_descr', 'website_url'.
+            The feed will not be (re)generated.
+            \nRelevant pointer:
+            $POINTER_PV
+            """)
         return nothing
     end
 
diff --git a/src/manager/write_page.jl b/src/manager/write_page.jl
index f108bb27..f097e2ed 100644
--- a/src/manager/write_page.jl
+++ b/src/manager/write_page.jl
@@ -99,8 +99,8 @@ function write_page(output_path::AS, content::AS;
         end
     end
 
-    # the previous call possibly resolved a {{paginate}} which will have
-    # stored a :paginate_itr var, so we must branch on that
+    # the previous convert call possibly resolved a {{paginate}} which will
+    # have stored a :paginate_itr var, so we must branch on that
     if !isnothing(locvar(:paginate_itr))
         union!(PAGINATED, (outdir,))
         name    = locvar(:paginate_itr)
diff --git a/src/utils/vars.jl b/src/utils/vars.jl
index 10cd2fdd..55dd5ba6 100644
--- a/src/utils/vars.jl
+++ b/src/utils/vars.jl
@@ -395,8 +395,7 @@ function set_vars!(vars::PageVars, assignments::Vector{Pair{String,String}}
             if check_type(type_tmp, acc_types)
                 vars[key] = Pair(tmp, acc_types)
             else
-                @warn "Page var '$key' (type(s): $acc_types) can't be set " *
-                      "to value '$tmp' (type: $type_tmp). Assignment ignored."
+                mddef_warn(key, tmp, acc_types)
             end
         else
             # there is no key, so directly assign, the type is not checked
diff --git a/src/utils/warnings.jl b/src/utils/warnings.jl
new file mode 100644
index 00000000..dac1597a
--- /dev/null
+++ b/src/utils/warnings.jl
@@ -0,0 +1,54 @@
+# Specific warning that will occur in more than one setting
+# otherwise warnings that happen in only a single context just use
+# `print_warning`
+
+hfun_unknown_arg1_warn(name, argname) = """
+    A h-function call '{{$name $argname}}'$(ifelse(String(name)=="fill",
+    " (or '{{$argname}}')", "")) has argument '$argname' which
+    doesn't match a page variable available in the current scope. It might have
+    been misspelled or should be defined via a '@def $argname = ...' either
+    locally on the page or globally in 'config.md'.
+    \nRelevant pointers:
+    $POINTER_PV
+    """ |> print_warning
+
+hfun_misc_warn(name, msg) = """
+    A h-function call '{{$name ...}}' has some issue(s).
+    $msg
+    """ |> print_warning
+
+mddef_warn(key, value, acc) = """
+    Page var '$key' (type(s): $acc) cannot be set to value '$value' of type
+    $(typeof(value)). That assignment will be ignored.
+    \nRelevant pointers:
+    $POINTER_PV
+    """ |> print_warning
+
+config_warn() = """
+    No 'config.md' file found. It is recommended to keep one.
+    \nRelevant pointers:
+    $POINTER_WORKFLOW
+    """ |> print_warning
+
+# --- utils ---
+
+function get_source()
+    # context
+    source = FD_ENV[:SOURCE]
+    isempty(source) || return source
+    return "unknown"  # unlikely outside of testing
+end
+
+printyb(msg) = printstyled(msg, color=:yellow, bold=true)
+
+function print_warning(msg)
+    FD_ENV[:SHOW_WARNINGS] || return
+    printyb("┌ Franklin Warning: ")
+    print("in <$(get_source())>\n")
+    for line in split(strip(msg), '\n')
+        printyb("│ ")
+        println(line)
+    end
+    printyb("└\n")
+    return
+end


Test patch:
diff --git a/test/converter/html/html.jl b/test/converter/html/html.jl
index f15c6deb..c7923766 100644
--- a/test/converter/html/html.jl
+++ b/test/converter/html/html.jl
@@ -16,15 +16,28 @@
     @test F.convert_html(hs) == "Some text then INPUT1 and\n\nother stuff\n\nfinal text\n"
 
     for expr in ("isdef", "ifdef")
+        local  hs
         hs = """abc {{$expr no}}yada{{end}} def"""
         @test F.convert_html(hs) == "abc  def"
     end
 
     hs = raw"""abc {{ fill nope }} ... """
-    @test (@test_logs (:warn, "I found a '{{fill nope}}' but I do not know the variable 'nope'. Ignoring.") F.convert_html(hs))  == "abc  ... "
+    global r; r=""; s = @capture_out begin
+        global r
+        r = F.convert_html(hs)
+    end
+    @test r == "abc  ... "
+    @test occursin("h-function call '{{fill nope}}'", s)
+    @test occursin("should be defined via a '@def nope = ...'", s)
 
     hs = raw"""unknown fun {{ unknown fun }} and see."""
-    @test (@test_logs (:warn, "I found a function block '{{unknown ...}}' but I don't recognise the function name. Ignoring.") F.convert_html(hs)) == "unknown fun  and see."
+     global r; r=""; s = @capture_out begin
+         global r
+         r = F.convert_html(hs)
+     end
+     @test r ==  "unknown fun  and see."
+     @test occursin("A block '{{unknown ...}}' was found but", s)
+     @test occursin("defined in 'utils.jl'.", s)
 end
 
 
@@ -38,7 +51,14 @@ end
     @test F.convert_html(hs) == "Trying to insert: some random text to insert and see.\n"
 
     hs = raw"""Trying to insert: {{ insert nope.rnd }} and see."""
-    @test (@test_logs (:warn, "I found an {{insert ...}} block and tried to insert '$(joinpath(F.PATHS[:src_html], "nope.rnd"))' but I couldn't find the file. Ignoring.") F.convert_html(hs)) == "Trying to insert:  and see."
+
+    global r = ""; s = @capture_out begin
+        global r
+        r = F.convert_html(hs)
+    end
+    @test occursin("h-function call '{{insert ...}}'", s)
+    @test occursin("Couldn't find the file", s)
+    @test r == "Trying to insert:  and see."
 end
 
 @testset "h-insert-fs2" begin
@@ -51,7 +71,13 @@ end
     @test F.convert_html(hs) == "Trying to insert: some random text to insert and see.\n"
 
     hs = raw"""Trying to insert: {{ insert nope.rnd }} and see."""
-    @test (@test_logs (:warn, "I found an {{insert ...}} block and tried to insert '$(joinpath(F.PATHS[:layout], "nope.rnd"))' but I couldn't find the file. Ignoring.") F.convert_html(hs)) == "Trying to insert:  and see."
+    global r = ""; s = @capture_out begin
+        global r
+        r = F.convert_html(hs)
+    end
+    @test occursin("h-function call '{{insert ...}}'", s)
+    @test occursin("Couldn't find the file", s)
+    @test r == "Trying to insert:  and see."
 end
 
 
diff --git a/test/coverage/extras1.jl b/test/coverage/extras1.jl
index 9cde34d4..c2a92dbc 100644
--- a/test/coverage/extras1.jl
+++ b/test/coverage/extras1.jl
@@ -7,7 +7,14 @@ end
 @testset "Conv-html" begin
     @test_throws F.HTMLFunctionError F.convert_html("{{insert bb cc}}")
     @test_throws F.HTMLFunctionError F.convert_html("{{href aa}}")
-    @test (@test_logs (:warn, "Unknown dictionary name aa in {{href ...}}. Ignoring") F.convert_html("{{href aa bb}}")) == "<b>??</b>"
+
+    global r = ""; s = @capture_out begin
+        global r
+        r = F.convert_html("{{href aa bb}}")
+    end
+    @test r == "<b>??</b>"
+    @test occursin("Unknown reference dictionary 'aa'", s)
+
     @test_throws F.HTMLBlockError F.convert_html("{{if asdf}}{{end}}")
     @test_throws F.HTMLBlockError F.convert_html("{{if asdf}}")
     @test_throws F.HTMLBlockError F.convert_html("{{isdef asdf}}")
@@ -18,7 +25,12 @@ end
     s = """
         @def blah
         """
-    @test (@test_logs (:warn, "Found delimiters for an @def environment but it didn't have the right @def var = ... format. Verify (ignoring for now).") (s |> fd2html_td)) == ""
+    global r = ""; s = @capture_out begin
+        global r
+        r = s |> fd2html_td
+    end
+    @test r == ""
+    @test occursin("Delimiters for an '@def ...'", s)
 
     s = """
         Blah
@@ -36,11 +48,12 @@ end
 @testset "RSS" begin
     F.set_var!(F.GLOBAL_VARS, "website_descr", "")
     F.RSS_DICT["hello"] = F.RSSItem("","","","","","","",Date(1))
-    @test (@test_logs (:warn, """
-              I found RSS items but the RSS feed is not properly described:
-              at least one of the following variables has not been defined in
-              your config.md: `website_title`, `website_descr`, `website_url`.
-              The feed will not be (re)generated.""") F.rss_generator()) === nothing
+    global r = ""; s = @capture_out begin
+        global r
+        r = F.rss_generator()
+    end
+    @test r === nothing
+    @test occursin("RSS items were found but", s)
 end
 
 
diff --git a/test/eval/codeblock.jl b/test/eval/codeblock.jl
index aee7d241..65c181b4 100644
--- a/test/eval/codeblock.jl
+++ b/test/eval/codeblock.jl
@@ -75,9 +75,9 @@ end
         a = 5
         b = 7
         ```""")
-    @test @test_logs (:warn, "Evaluation of non-Julia code blocks is not yet supported.") F.resolve_code_block(c) ==
-        """<pre><code class="language-python">a = 5
-        b = 7</code></pre>"""
+
+    s = @capture_out F.resolve_code_block(c)
+    @test occursin("Evaluation of non-Julia code", s)
 
     # should eval
     bak = pwd()
diff --git a/test/eval/eval.jl b/test/eval/eval.jl
index 29c8c639..358849f2 100644
--- a/test/eval/eval.jl
+++ b/test/eval/eval.jl
@@ -61,14 +61,18 @@ end
         ```
         done.
         """
+    global h
     h = ""
-    @test_logs (:warn, "Evaluation of non-Julia code blocks is not yet supported.") (h = s |> seval)
-
+    s = @capture_out begin
+        global h
+        h = s |> seval
+    end
     @test isapproxstr(h, raw"""
             <p>Simple code:</p>
             <pre><code class="language-python">a = 5
             print(a**2)</code></pre>
             <p>done.</p>""")
+    @test occursin("non-Julia code blocks", s)
 end
 
 @testset "Eval (rinput)" begin
@@ -197,19 +201,22 @@ end
         """
     global h
     h = ""
-    @test_logs (:warn, "There was an error of type DomainError running the code.") (global h; h = s |> seval)
-    # errors silently
-    if VERSION >= v"1.2"
-        @test h // raw"""
-                    <p>Simple code:</p>
-                    <pre><code class="language-julia">sqrt(-1)</code></pre>
-                    <p>then:</p>
-                    <pre><code class="plaintext">DomainError with -1.0:
-                    sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
-                    </code></pre>
-                    <p>done.</p>
-                    """
+    s = @capture_out begin
+        global h
+        h = s |> seval
     end
+    @test occursin("of type 'DomainError' when running", s)
+
+    # errors silently
+    @test h // raw"""
+                <p>Simple code:</p>
+                <pre><code class="language-julia">sqrt(-1)</code></pre>
+                <p>then:</p>
+                <pre><code class="plaintext">DomainError with -1.0:
+                sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
+                </code></pre>
+                <p>done.</p>
+                """
 end
 
 @testset "Eval (nojl)" begin
@@ -220,8 +227,14 @@ end
         ```
         done.
         """
-
-    @test (@test_logs (:warn, "Evaluation of non-Julia code blocks is not yet supported.") h |> seval) // raw"""
+    global r
+    r = ""
+    s = @capture_out begin
+        global r
+        r = h |> seval
+    end
+    @test occursin("non-Julia code blocks", s)
+    @test r // raw"""
             <p>Simple code:</p>
             <pre><code class="language-python">sqrt(-1)</code></pre>
             <p>done.</p>"""
diff --git a/test/eval/eval_fs2.jl b/test/eval/eval_fs2.jl
index 15e1c8d5..9d03a96a 100644
--- a/test/eval/eval_fs2.jl
+++ b/test/eval/eval_fs2.jl
@@ -61,8 +61,13 @@ end
         ```
         done.
         """
+    global h
     h = ""
-    @test_logs (:warn, "Evaluation of non-Julia code blocks is not yet supported.") (h = s |> seval)
+    s = @capture_out begin
+        global h
+        h = s |> seval
+    end
+    @test occursin("non-Julia code blocks", s)
 
     @test isapproxstr(h, raw"""
                         <p>Simple code:</p>
@@ -197,18 +202,19 @@ end
         """
     global h
     h = ""
-    @test_logs (:warn, "There was an error of type DomainError running the code.") (global h; h = s |> seval)
-    # errors silently
-    if VERSION >= v"1.2"
-        @test h // raw"""
-                    <p>Simple code:</p>
-                    <pre><code class="language-julia">sqrt(-1)</code></pre>
-                    <p>then:</p>
-                    <pre><code class="plaintext">DomainError with -1.0:
-                    sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
-                    </code></pre>
-                    <p>done.</p>"""
+    s = @capture_out begin
+        global h
+        h = s |> seval
     end
+    @test h // raw"""
+                <p>Simple code:</p>
+                <pre><code class="language-julia">sqrt(-1)</code></pre>
+                <p>then:</p>
+                <pre><code class="plaintext">DomainError with -1.0:
+                sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
+                </code></pre>
+                <p>done.</p>"""
+    @test occursin("'DomainError' when", s)
 end
 
 @testset "Eval (nojl)" begin
@@ -219,8 +225,14 @@ end
         ```
         done.
         """
-
-    @test (@test_logs (:warn, "Evaluation of non-Julia code blocks is not yet supported.") h |> seval) // raw"""
+    global r
+    r = ""
+    s = @capture_out begin
+        global r
+        r = h |> seval
+    end
+    @test occursin("non-Julia", s)
+    @test r// raw"""
         <p>Simple code:</p>
         <pre><code class="language-python">sqrt(-1)</code></pre>
         <p>done.</p>
diff --git a/test/eval/run.jl b/test/eval/run.jl
index ace9f3b0..5ed34352 100644
--- a/test/eval/run.jl
+++ b/test/eval/run.jl
@@ -69,12 +69,11 @@ end
         a = sqrt(-1)
         b = 7
         """
-    @test (@test_logs (:warn, "There was an error of type DomainError running the code.") F.run_code(mod, c, junk)) === nothing
-    if VERSION >= v"1.2"
-        @test read(junk, String) == """DomainError with -1.0:
-            sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
-            """
-    end
+
+    s = @capture_out F.run_code(mod, c, junk)
+    @test occursin("Warning: in <input string>", s)
+    @test occursin("error of type 'DomainError'", s)
+    @test occursin("Checking the output files", s)
 end
 
 @testset "i462" begin
diff --git a/test/integration/literate.jl b/test/integration/literate.jl
index 5446f7d7..89891cec 100644
--- a/test/integration/literate.jl
+++ b/test/integration/literate.jl
@@ -111,5 +111,11 @@ end
     s = raw"""
         \literate{/foo}
         """
-    @test @test_logs (:warn, "File not found when trying to convert a literate file ($(joinpath(F.PATHS[:folder], "foo.jl"))).") (s |> fd2html_td) == """<p><span style="color:red;">// Literate file matching '/foo' not found. //</span></p>\n"""
+
+    global r = ""; s = @capture_out begin
+        global r
+        r = s |> fd2html_td
+    end
+    @test r == """<p><span style="color:red;">// Literate file matching '/foo' not found. //</span></p>\n"""
+    @test occursin("File not found when", s)
 end
diff --git a/test/integration/literate_fs2.jl b/test/integration/literate_fs2.jl
index f0a3cdfa..951ef82e 100644
--- a/test/integration/literate_fs2.jl
+++ b/test/integration/literate_fs2.jl
@@ -148,5 +148,11 @@ end
     s = raw"""
         \literate{/foo}
         """
-    @test @test_logs (:warn, "File not found when trying to convert a literate file ($(joinpath(F.PATHS[:folder], "foo.jl"))).") (s |> fd2html_td) == """<p><span style="color:red;">// Literate file matching '/foo' not found. //</span></p>\n"""
+
+    global r = ""; s = @capture_out begin
+        global r
+        r = s |> fd2html_td
+    end
+    @test r == """<p><span style="color:red;">// Literate file matching '/foo' not found. //</span></p>\n"""
+    @test occursin("File not found when", s)
 end
diff --git a/test/manager/paginate.jl b/test/manager/paginate.jl
index 9fa4721a..54bd1138 100644
--- a/test/manager/paginate.jl
+++ b/test/manager/paginate.jl
@@ -60,35 +60,7 @@
         FOOT"""
 
     # WARNINGS
-    write(joinpath(td, "foo.md"), raw"""
-        @def a = ["<li>Item $i</li>" for i in 1:10]
-        Some content
-        {{paginate abc 4}}
-        """)
-    @test_logs (:warn, "In a {{paginate ...}} block, I couldn't recognise the name of the iterable. Nothing will get printed as a result.") serve(single=true)
-    write(joinpath(td, "foo.md"), raw"""
-        @def a = ["<li>Item $i</li>" for i in 1:10]
-        Some content
-        {{paginate a iehva}}
-        """)
-    @test_logs (:warn, "In a {{paginate ...}} block, I couldn't parse the number of items per page. Defaulting to 10.") serve(single=true)
-    write(joinpath(td, "foo.md"), raw"""
-        @def a = ["<li>Item $i</li>" for i in 1:10]
-        Some content
-        {{paginate a -5}}
-        """)
-    @test_logs (:warn, "In a {{paginate ...}} block, the number of items per page is non-positive, defaulting to 10.") serve(single=true)
-    write(joinpath(td, "foo.md"), raw"""
-        @def a = ["<li>Item $i</li>" for i in 1:10]
-        Some content
-        ~~~<ul>~~~
-        {{paginate a 4}}
-        ~~~</ul>~~~
-        ~~~<ul>~~~
-        {{paginate a 4}}
-        ~~~</ul>~~~
-        """)
-    @test_logs (:warn, "It looks like you have multiple calls to {{paginate ...}} on the page; only one is supported. Verify.") serve(single=true)
+    # (see warnings)
 
     # ERRORS
     write(joinpath(td, "foo.md"), raw"""
diff --git a/test/manager/rss.jl b/test/manager/rss.jl
index e7126f06..6833970f 100644
--- a/test/manager/rss.jl
+++ b/test/manager/rss.jl
@@ -33,7 +33,6 @@ end
     @test item.pubDate == Date(1)
 
     F.set_var!(F.LOCAL_VARS, "rss_title", "")
-    @test @test_logs (:warn, "Found an RSS description but no title for page /hey/ho.html.") F.add_rss_item().title == ""
 
     @test F.RSS_DICT["/hey/ho.html"].description == item.description
 
@@ -67,7 +66,6 @@ end
     @test item.pubDate == Date(1)
 
     F.set_var!(F.LOCAL_VARS, "rss_title", "")
-    @test @test_logs (:warn, "Found an RSS description but no title for page /hey/ho/index.html.") F.add_rss_item().title == ""
 
     @test F.RSS_DICT["/hey/ho/index.html"].description == item.description
 
diff --git a/test/manager/utils.jl b/test/manager/utils.jl
index 9c20f3c2..5cc624cb 100644
--- a/test/manager/utils.jl
+++ b/test/manager/utils.jl
@@ -55,7 +55,6 @@ end
     F.process_config()
     @test F.GLOBAL_VARS["author"].first == "Stefan Zweig"
     rm(temp_config)
-    @test_logs (:warn, "I didn't find a config file. Ignoring.") F.process_config()
     # testing write
     head = "head"
     pg_foot = "\npage_foot"
diff --git a/test/manager/utils_fs2.jl b/test/manager/utils_fs2.jl
index 21f92680..32128aea 100644
--- a/test/manager/utils_fs2.jl
+++ b/test/manager/utils_fs2.jl
@@ -56,7 +56,7 @@ end
     F.process_config()
     @test F.GLOBAL_VARS["author"].first == "Stefan Zweig"
     rm(temp_config)
-    @test_logs (:warn, "I didn't find a config file. Ignoring.") F.process_config()
+
     # testing write
     head = "head"
     pg_foot = "\npage_foot"
diff --git a/test/runtests.jl b/test/runtests.jl
index bc2da6ee..f8777420 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -1,4 +1,5 @@
-using Franklin, Test, Markdown, Dates, Random, Literate, DelimitedFiles
+using Franklin, Test, Markdown, Dates, Random
+using Literate, DelimitedFiles, Suppressor
 const F = Franklin
 const R = @__DIR__
 const D = joinpath(dirname(dirname(pathof(Franklin))), "test", "_dummies")
@@ -17,6 +18,7 @@ include("utils/paths_vars.jl"); include("test_utils.jl")
 println("UTILS-2")
 include("utils/misc.jl")
 include("utils/errors.jl")
+include("utils/warnings.jl")
 include("utils/html.jl")
 include("regexes.jl")
 println("🍺")
diff --git a/test/test_utils.jl b/test/test_utils.jl
index 2df30b0c..b2ce53f3 100644
--- a/test/test_utils.jl
+++ b/test/test_utils.jl
@@ -144,7 +144,7 @@ function explore_h_steps(hs, allvars=F.PageVars())
     return steps
 end
 
-gotd() = (flush_td(); cd(td); F.FOLDER_PATH[] = td)
+gotd() = (flush_td(); cd(td); F.FOLDER_PATH[] = td; set_globals())
 
 function fs1()
     F.FD_ENV[:STRUCTURE] = v"0.1"
@@ -181,3 +181,5 @@ function fs2()
 end
 
 fdi(s) = fd2html(s; internal=true)
+
+resource() = (F.FD_ENV[:SOURCE] = ""; set_globals())
diff --git a/test/utils/paths_vars.jl b/test/utils/paths_vars.jl
index a7f47ae0..d77de9e9 100644
--- a/test/utils/paths_vars.jl
+++ b/test/utils/paths_vars.jl
@@ -42,8 +42,6 @@ cp(joinpath(dirname(dirname(pathof(Franklin))), "test", "_libs", "katex"), joinp
     @test d["a"].first == 5
     @test d["b"].first === nothing
 
-    @test_logs (:warn, "Page var 'a' (type(s): (Real,)) can't be set to value 'blah' (type: String). Assignment ignored.") F.set_vars!(d, ["a"=>"\"blah\""])
-
     @test_throws F.PageVariableError F.set_vars!(d, ["a"=> "sqrt(-1)"])
 
     # assigning new variables
diff --git a/test/utils/warnings.jl b/test/utils/warnings.jl
new file mode 100644
index 00000000..561cce02
--- /dev/null
+++ b/test/utils/warnings.jl
@@ -0,0 +1,85 @@
+@testset "warn-pv" begin
+    resource()
+    d = F.PageVars(
+    	"a" => 0.5 => (Real,),
+    	"b" => "hello" => (String, Nothing))
+    s = @capture_out F.set_vars!(d, ["a"=>"\"blah\""])
+    @test occursin("Franklin Warning: in <unknown>", s)
+    @test occursin("Page var 'a' (type(s): (Real,)) cannot", s)
+    @test occursin("assignment will be ignored",  s)
+end
+
+@testset "warn-config" begin
+    resource(); gotd()
+    s = @capture_out F.process_config()
+    @test occursin("Warning: in <config.md>", s)
+    @test occursin("No 'config.md' file found", s)
+end
+
+@testset "warn-rss" begin
+    resource(); gotd()
+    empty!(F.RSS_DICT)
+    F.set_var!(F.GLOBAL_VARS, "website_title", "Website title")
+    F.set_var!(F.GLOBAL_VARS, "website_descr", "Website descr")
+    F.set_var!(F.GLOBAL_VARS, "website_url", "https://github.com/tlienart/Franklin.jl/")
+    F.def_LOCAL_VARS!()
+    set_curpath("hey/ho.md")
+    F.set_var!(F.LOCAL_VARS, "rss_title", "")
+    s = @capture_out F.add_rss_item()
+    @test occursin("Warning: in <input string>", s)
+    @test occursin("An RSS description was found but without title", s)
+    @test occursin("for page '/hey/ho/index.html'.", s)
+end
+
+@testset "warn-paginate" begin
+    resource(); gotd()
+    write(joinpath(td, "config.md"), "@def aa = 5")
+    mkpath(joinpath(td, "_css"))
+    mkpath(joinpath(td, "_layout"))
+    write(joinpath(td, "_layout", "head.html"), "HEAD\n")
+    write(joinpath(td, "_layout", "foot.html"), "\nFOOT\n")
+    write(joinpath(td, "_layout", "page_foot.html"), "\nPG_FOOT\n")
+    write(joinpath(td, "index.md"), "INDEX")
+    write(joinpath(td, "foo.md"), raw"""
+        @def a = ["<li>Item $i</li>" for i in 1:10]
+        Some content
+        {{paginate abc 4}}
+        """)
+    s = @capture_out serve(single=true)
+    @test occursin("Franklin Warning: in <foo.md>", s)
+    @test occursin("A h-function call '{{paginate ...}}' has some", s)
+    @test occursin("The page variable 'abc' does not match", s)
+
+    write(joinpath(td, "foo.md"), raw"""
+        @def a = ["<li>Item $i</li>" for i in 1:10]
+        Some content
+        {{paginate a iehva}}
+        """)
+    s = @capture_out serve(single=true)
+    @test occursin("Failed to parse", s)
+    @test occursin("(given: 'iehva')", s)
+    @test occursin("Setting to", s)
+
+    write(joinpath(td, "foo.md"), raw"""
+        @def a = ["<li>Item $i</li>" for i in 1:10]
+        Some content
+        {{paginate a -5}}
+        """)
+    s = @capture_out serve(single=true)
+    @test occursin("Non-positive number", s)
+    @test occursin("('-5' read from -5)", s)
+    @test occursin("Setting to", s)
+
+    write(joinpath(td, "foo.md"), raw"""
+        @def a = ["<li>Item $i</li>" for i in 1:10]
+        Some content
+        ~~~<ul>~~~
+        {{paginate a 4}}
+        ~~~</ul>~~~
+        ~~~<ul>~~~
+        {{paginate a 4}}
+        ~~~</ul>~~~
+        """)
+    s = @capture_out serve(single=true)
+    @test occursin("Multiple calls to", s)
+end