Repository: tlienart/Franklin.jl
Pull request #718 (2020-12-03 20:51:33)

Print stack trace
When using Franklin, I usually know where the error is coming from, so then the current warning which only shows the error type is fine. However, at times I duly miss the stacktrace and am completely in the dark about the cause of an error.

Currently, this PR proposes to print the stacktrace to `stderr`. I think this is more convenient for the user than storing it in `outf`. (I also don't think the `outf` has ever been of much help for me. It typically contains the error I already had in the REPL in one file, and `nothing` in the other.)

The response is now something like 
```
DomainError with -1.0:
sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
 [1] throw_complex_domainerror(::Symbol, ::Float64) at ./math.jl:33
 [2] sqrt at ./math.jl:573 [inlined]
 [3] sqrt(::Int64) at ./math.jl:599
 [4] top-level scope at none:1
 [5] eval at ./boot.jl:331 [inlined]
 [6] (::Franklin.var"#96#98"{Module,Array{Any,1},Int64})() at /home/rik/FR/src/eval/run.jl:64
 [7] redirect_stdout(::Franklin.var"#96#98"{Module,Array{Any,1},Int64}, ::IOStream) at ./stream.jl:1150
 [8] (::Franklin.var"#95#97"{Module,String,Array{Any,1},Int64})(::IOStream) at /home/rik/FR/src/eval/run.jl:60
 [9] open(::Franklin.var"#95#97"{Module,String,Array{Any,1},Int64}, ::String, ::Vararg{String,N} where N; kwargs::Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}) at ./io.jl:325
 [10] open at ./io.jl:323 [inlined]
 [11] run_code(::Module, ::String, ::String; warn_err::Bool, strip_code::Bool) at /home/rik/FR/src/eval/run.jl:56
 [12] run_code(::Module, ::String, ::String) at /home/rik/FR/src/eval/run.jl:49
 [13] macro expansion at /home/rik/FR/test/eval/run.jl:78 [inlined]
 [14] macro expansion at /buildworker/worker/package_linux64/build/usr/share/julia/stdlib/v1.5/Test/src/Test.jl:1115 [inlined]
 [15] top-level scope at /home/rik/FR/test/eval/run.jl:26
 [16] include(::String) at ./client.jl:457
 [17] top-level scope at /home/rik/FR/test/runtests.jl:50
 [18] include(::String) at ./client.jl:457
 [19] top-level scope at none:6
 [20] eval(::Module, ::Any) at ./boot.jl:331
 [21] exec_options(::Base.JLOptions) at ./client.jl:272
 [22] _start() at ./client.jl:506
┌ Franklin Warning: in <input string>
│ There was an error of type 'DomainError' when running a code block.
│ Checking the output files '/tmp/jl_yNUxkV.(out|res)'
│ might be helpful to understand and solve the issue.
│
│ Relevant pointers:
│ - evaluation of Julia code blocks: https://franklinjl.org/code/
└
```

The code is not ready to be merged yet, it needs more testing and possibly a bit of tweaking of the design.


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 00000000..ecf6175b
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,47 @@
+name: CI
+on:
+  pull_request:
+    branches:
+      - master
+      - dev
+  push:
+    branches:
+      - master
+      - dev
+    tags: '*'
+jobs:
+  test:
+    name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }}
+    runs-on: ${{ matrix.os }}
+    strategy:
+      fail-fast: false
+      matrix:
+        version:
+          - '1.3'
+          - '1'
+        os:
+          - ubuntu-latest
+        arch:
+          - x64
+    steps:
+      - uses: actions/checkout@v2
+      - uses: julia-actions/setup-julia@v1
+        with:
+          version: ${{ matrix.version }}
+          arch: ${{ matrix.arch }}
+      - uses: actions/cache@v1
+        env:
+          cache-name: cache-artifacts
+        with:
+          path: ~/.julia/artifacts
+          key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }}
+          restore-keys: |
+            ${{ runner.os }}-test-${{ env.cache-name }}-
+            ${{ runner.os }}-test-
+            ${{ runner.os }}-
+      - uses: julia-actions/julia-buildpkg@v1
+      - uses: julia-actions/julia-runtest@v1
+      - uses: julia-actions/julia-processcoverage@v1
+      - uses: codecov/codecov-action@v1
+        with:
+          file: lcov.info
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 36e0976b..00000000
--- a/.travis.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-# Documentation: http://docs.travis-ci.com/user/languages/julia/
-language: julia
-os:
-  - linux
-julia:
-  - 1.3
-  - 1.5
-  - nightly
-matrix:
-  allow_failures:
-    - julia: nightly
-notifications:
-  email: false
-after_success:
-  # push coverage results to Codecov
-  - julia -e 'using Pkg; pkg"add Coverage"; using Coverage; Codecov.submit(Codecov.process_folder())'
diff --git a/Project.toml b/Project.toml
index 10ef4515..f0e2a4e4 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.10.13"
+version = "0.10.16"
 
 [deps]
 Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
diff --git a/README.md b/README.md
index 92d1f59f..096a995b 100644
--- a/README.md
+++ b/README.md
@@ -8,13 +8,9 @@
 <p align="center">
   <img src="https://img.shields.io/badge/lifecycle-maturing-blue.svg"
        alt="Lifecycle">
-  <a href="https://travis-ci.com/tlienart/Franklin.jl">
-    <img src="https://travis-ci.com/tlienart/Franklin.jl.svg?branch=master"
-         alt="Build Status (Linux)">
-  </a>
-  <a href="https://ci.appveyor.com/project/tlienart/Franklin-jl">
-    <img src="https://ci.appveyor.com/api/projects/status/github/tlienart/Franklin.jl?branch=master&svg=true"
-         alt="Build Status (Windows)">
+  <a href="https://github.com/tlienart/Franklin.jl/actions">
+    <img src="https://github.com/tlienart/Franklin.jl/workflows/CI/badge.svg"
+         alt="Build Status">
   </a>
   <a href="http://codecov.io/github/tlienart/Franklin.jl?branch=master">
     <img src="http://codecov.io/github/tlienart/Franklin.jl/coverage.svg?branch=master"
@@ -26,7 +22,7 @@
 Franklin is a simple **static site generator** (SSG) oriented towards technical blogging (code, maths, ...), flexibility and extensibility.
 The base syntax is plain markdown with a few extensions such as the ability to define and use LaTeX-like commands in or outside of maths environments and the possibility to evaluate code  blocks on the fly.
 
-Franklin has a channel/stream **#franklin** on the Julia slack, this is the best place to ask usage question.
+Franklin has a channel **#franklin** on the Julia slack, this is the best place to ask usage question.
 
 ## Docs
 
diff --git a/appveyor.yml b/appveyor.yml
deleted file mode 100644
index 19fd1c2d..00000000
--- a/appveyor.yml
+++ /dev/null
@@ -1,28 +0,0 @@
-environment:
-  matrix:
-  - julia_version: 1.3
-  - julia_version: 1.5
-  - julia_version: nightly
-
-platform:
-  - x64 # 64-bit
-
-branches:
-  only:
-    - master
-
-notifications:
-  - provider: Email
-    on_build_success: false
-    on_build_failure: false
-    on_build_status_changed: false
-
-install:
-  - ps: iex ((new-object net.webclient).DownloadString("https://raw.githubusercontent.com/JuliaCI/Appveyor.jl/version-1/bin/install.ps1"))
-
-build_script:
-  - C:\julia\bin\julia -e "%JL_BUILD_SCRIPT%"
-
-test_script:
-- echo "%JL_TEST_SCRIPT%"
-  - C:\julia\bin\julia -e "%JL_TEST_SCRIPT%"
diff --git a/src/converter/html/html.jl b/src/converter/html/html.jl
index 9a0fa2ca..ada55034 100644
--- a/src/converter/html/html.jl
+++ b/src/converter/html/html.jl
@@ -47,9 +47,9 @@ 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}
-    FD_ENV[:SOURCE] = "input string"
     isempty(st) && return (st, LittleDict())
     if !internal
+        FD_ENV[:SOURCE] = "input string"
         empty!(ALL_PAGE_VARS)
         FOLDER_PATH[] = isempty(dir) ? mktempdir() : dir
         set_paths!()
diff --git a/src/converter/markdown/md.jl b/src/converter/markdown/md.jl
index e194852d..a2b6b0c4 100644
--- a/src/converter/markdown/md.jl
+++ b/src/converter/markdown/md.jl
@@ -169,11 +169,14 @@ function convert_md(mds::AbstractString,
     (:convert_md, "hstring: '$hstring'") |> logger
 
     # final var adjustment, infer title if not given
+    rpath = splitext(locvar(:fd_rpath))[1]
     if isnothing(locvar(:title)) && !isempty(PAGE_HEADERS)
         title = first(values(PAGE_HEADERS))[1]
         set_var!(LOCAL_VARS, "title", title)
-        ALL_PAGE_VARS[splitext(locvar(:fd_rpath))[1]]["title"] =
-            deepcopy(LOCAL_VARS["title"])
+    end
+    # Copy vars to allpagevars to make them more easily accessible to other pages
+    if !(isrecursive || isinternal)
+        ALL_PAGE_VARS[rpath] = deepcopy(LOCAL_VARS)
     end
 
     # Return the string
@@ -199,6 +202,9 @@ function convert_md_math(ms::AS, lxdefs::Vector{LxDef}=Vector{LxDef}(),
                          offset::Int=0)::String
     # if a substring is given, copy it as string
     ms = String(ms)
+
+    (:convert_md_math, "ms: '$ms'") |> logger
+
     #
     # Parsing of the markdown string
     # (to find latex command, latex definitions, math envs etc.)
@@ -207,14 +213,21 @@ function convert_md_math(ms::AS, lxdefs::Vector{LxDef}=Vector{LxDef}(),
     #> 1. Tokenize (with restricted set)
     tokens = find_tokens(ms, MD_TOKENS_LX, MD_1C_TOKENS_LX)
 
+    (:convert_md_math, "tokens: '$tokens'") |> logger
+
     #> 2. Find braces and drop line returns thereafter
     blocks, tokens = find_all_ocblocks(tokens, MD_OCB_ALL, inmath=true)
+    deactivate_inner_blocks!(blocks)
     braces = filter(β -> β.name == :LXB, blocks)
 
+    (:convert_md_math, "braces: '$braces'") |> logger
+
     #> 3. Find latex envs and commands (indicate we're in a math environment + offset)
     lxenvs, tokens = find_lxenvs(tokens, lxdefs, braces, offset; inmath=true)
     lxcoms, _      = find_lxcoms(tokens, lxdefs,  braces, offset; inmath=true)
 
+    (:convert_md_math, "lxcoms: '$lxcoms'") |> logger
+
     #
     # Forming of the html string
     # (see `form_inter_md`, it's similar but simplified since there are fewer
diff --git a/src/converter/markdown/mddefs.jl b/src/converter/markdown/mddefs.jl
index bea647a6..bb681e61 100644
--- a/src/converter/markdown/mddefs.jl
+++ b/src/converter/markdown/mddefs.jl
@@ -93,11 +93,7 @@ function process_mddefs(blocks::Vector{OCBlock}, isconfig::Bool,
         set_var!(LOCAL_VARS, "hasmath", math)
     end
 
-    # copy the page vars to ALL_PAGE_VARS so that they can be accessed
-    # by other pages via `pagevar`.
-    ALL_PAGE_VARS[rpath] = deepcopy(LOCAL_VARS)
-
-    (:process_mddefs, "assignments done & copy to ALL_PAGE_VARS") |> logger
+    (:process_mddefs, "assignments done") |> logger
 
     # TAGS
     tags = Set(refstring.(locvar(:tags)))
diff --git a/src/eval/run.jl b/src/eval/run.jl
index d3be3c89..582b78e1 100644
--- a/src/eval/run.jl
+++ b/src/eval/run.jl
@@ -26,6 +26,21 @@ end
 """
 $SIGNATURES
 
+Returns only the stack traces which are related to the user's code.
+This means removing stack traces pointing to Franklin's code.
+"""
+function trim_stacktrace(stacktrace::String)
+    # Franklin's stack traces always start with something like
+    # `(::Franklin.var"#96#98"{...})() at .../src/eval/run.jl:65`
+    rx = r"\[\d+\]\s\(\:\:Franklin.var(?:.*?)src\/eval\/run.jl"
+    first_match_start = first(findfirst(rx, stacktrace))
+    # Keep only everything before the regex match.
+    stacktrace[1:first_match_start-3]
+end
+
+"""
+$SIGNATURES
+
 Run some code in a given module while redirecting stdout to a given path.
 Return the result of the evaluation or `nothing` if the code was empty or
 the evaluation failed.
@@ -52,6 +67,7 @@ function run_code(mod::Module, code::AS, out_path::AS;
     ne   = length(exs)
     res  = nothing # to capture final result
     err  = nothing
+    stacktrace = nothing
     ispath(out_path) || mkpath(dirname(out_path))
     open(out_path, "w") do outf
         if !FD_ENV[:SILENT_MODE]
@@ -67,6 +83,10 @@ function run_code(mod::Module, code::AS, out_path::AS;
                     showerror(io, e)
                     println(String(take!(io)))
                     err = typeof(e)
+
+                    exc, bt = last(Base.catch_stack())
+                    stacktrace = sprint(showerror, exc, bt)
+
                     break
                 end
                 e += 1
@@ -75,8 +95,6 @@ function run_code(mod::Module, code::AS, out_path::AS;
     end
     # if there was an error, return nothing and possibly show warning
     if !isnothing(err)
-        # TODO: add more informative message, maybe show type of error
-        # + parent path
         FD_ENV[:SILENT_MODE] || print("\n")
         warn_err && print_warning("""
             There was an error of type '$err' when running a code block.
@@ -84,6 +102,8 @@ function run_code(mod::Module, code::AS, out_path::AS;
             might be helpful to understand and solve the issue.
             \nRelevant pointers:
             $POINTER_EVAL
+            \nDetails:
+            $(trim_stacktrace(stacktrace))
             """)
         res = nothing
     end
diff --git a/src/manager/dir_utils.jl b/src/manager/dir_utils.jl
index 05d91d74..82ffbdb7 100644
--- a/src/manager/dir_utils.jl
+++ b/src/manager/dir_utils.jl
@@ -34,7 +34,7 @@ function form_output_path(base::AS, file::AS, case::Symbol)
         # file is index.html or 404.html --> keep the path
         # file is page.html  --> .../page/index.html
         fname = splitext(file)[1]
-        if fname ∉ ("index", "404")
+        if fname ∉ ("index", "404") && !endswith(fname, "/index")
             file = joinpath(fname, "index.html")
         end
     end
@@ -59,8 +59,6 @@ function _out_path(base::String)::String
        # path is not a 'special folder'
        outpath = replace(base, path(:folder) => path(:site))
    end
-    # if it doesn't exist, make the path
-    !ispath(outpath) && mkpath(outpath)
     return outpath
 end
 
diff --git a/src/manager/franklin.jl b/src/manager/franklin.jl
index 89166329..5fa7b212 100644
--- a/src/manager/franklin.jl
+++ b/src/manager/franklin.jl
@@ -246,12 +246,13 @@ function fd_fullpass(watched_files::NamedTuple)::Int
         end
         s += a
     end
+
     # re-evaluate delayed pages
     if !isempty(DELAYED)
         cp_DELAYED = copy(DELAYED)
         empty!(DELAYED) # so that functions are effectively applied
         for page in cp_DELAYED
-            case = Symbol(strip(splitext(page)[2], '.'))
+            case = Symbol(strip(splitext(page)[2], '.'))  # md / html
             fpair = path(:folder) => page
             a = process_file(case, fpair, head, pg_foot, foot)
             if a < 0 && FD_ENV[:PRERENDER] && FD_ENV[:NO_FAIL_PRERENDER]
diff --git a/src/manager/post_processing.jl b/src/manager/post_processing.jl
index 79989959..7d7d4ce4 100644
--- a/src/manager/post_processing.jl
+++ b/src/manager/post_processing.jl
@@ -81,6 +81,16 @@ function optimize(; prerender::Bool=true, minify::Bool=true, sig::Bool=false,
                   "pip. The output will not be minified."
         end
     end
+
+    #
+    # Clean up empty folders if any
+    #
+    for name in readdir(path(:site))
+        p = joinpath(path(:site), name)
+        isdir(p) || continue
+        isempty(readdir(p)) && rm(p)
+    end
+
     FD_ENV[:SUPPRESS_ERR] = false
     return ifelse(sig, succ, nothing)
 end
diff --git a/src/manager/rss_generator.jl b/src/manager/rss_generator.jl
index b29b946c..46b588e9 100644
--- a/src/manager/rss_generator.jl
+++ b/src/manager/rss_generator.jl
@@ -64,7 +64,7 @@ function add_rss_item()
     link  = url_curpage()
     title = jor("rss_title", "title")
     descr = jor("rss", "rss_description")
-
+    
     descr = fd2html(descr; internal=true) |> remove_html_ps
 
     author    = locvar(:rss_author)
diff --git a/src/parser/latex/blocks.jl b/src/parser/latex/blocks.jl
index 99c423b3..55774641 100644
--- a/src/parser/latex/blocks.jl
+++ b/src/parser/latex/blocks.jl
@@ -238,6 +238,10 @@ function find_lxcoms(tokens::Vector{Token}, lxdefs::Vector{LxDef},
         # 1. look for the definition given the command name
         lxname = τ.ss
         lxdefref, utils = get_lxdef_ref(lxname, lxdefs, inmath, offset)
+
+        (:find_lxcoms, "lxname: '$lxname'") |> logger
+        (:find_lxcoms, "utils: '$utils'") |> logger
+
         if utils
             # custom command defined in utils, take a single bracket
             lxnarg = 1
@@ -248,6 +252,9 @@ function find_lxcoms(tokens::Vector{Token}, lxdefs::Vector{LxDef},
             lxnarg = getindex(lxdefref).narg
         end
 
+        (:find_lxcoms, "lxnarg: '$lxnarg'") |> logger
+        (:find_lxcoms, "braces: '$(content.(braces))'") |> logger
+
         # 2. explore with # arguments
         # >> no arguments
         if lxnarg == 0
diff --git a/src/utils/misc.jl b/src/utils/misc.jl
index 4177ed5e..d24608bf 100644
--- a/src/utils/misc.jl
+++ b/src/utils/misc.jl
@@ -261,7 +261,8 @@ macro delay(defun)
     name = def[:name]
     body = def[:body]
     def[:body] = quote
-        union!(Franklin.DELAYED, (Franklin.FD_ENV[:SOURCE],))
+        src = Franklin.FD_ENV[:SOURCE]
+        splitext(src)[2] in (".md", ".html") && union!(Franklin.DELAYED, (src,))
         $body
     end
     esc(combinedef(def))
diff --git a/src/utils/vars.jl b/src/utils/vars.jl
index e109058b..afbfecd6 100644
--- a/src/utils/vars.jl
+++ b/src/utils/vars.jl
@@ -191,9 +191,9 @@ end
 """
 Dict to keep track of all pages and their vars. Each key is a relative path
 to a page, values are PageVars.
-The keys don't have the file extension so `"blog/pg1 => PageVars"`.
+The keys don't have the file extension so `"blog/pg1" => PageVars`.
 """
-const ALL_PAGE_VARS = Dict{String,PageVars}()
+const ALL_PAGE_VARS = LittleDict{String,PageVars}()
 
 """
     pagevar(rpath, name)


Test patch:
diff --git a/test/eval/run.jl b/test/eval/run.jl
index 5ed34352..0717d8b6 100644
--- a/test/eval/run.jl
+++ b/test/eval/run.jl
@@ -63,7 +63,23 @@ end
     @test isnothing(r)
     @test read(junk, String) == "x = 5\n"
 
-    # code with errorr
+    stacktrace = """
+        DomainError with -1.0:
+        sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
+        Stacktrace:
+         [1] throw_complex_domainerror(::Symbol, ::Float64) at ./math.jl:33
+         [2] sqrt at ./math.jl:573 [inlined]
+         [3] sqrt(::Int64) at ./math.jl:599
+         [4] top-level scope at none:1
+         [5] eval at ./boot.jl:331 [inlined]
+         [6] (::Franklin.var"#96#98"{Module,Array{Any,1},Int64})() at /home/rik/git/FR/src/eval/run.jl:71
+         [7] redirect_stdout(::Franklin.var"#96#98"{Module,Array{Any,1},Int64}, ::IOStream) at ./stream.jl:1150
+         [8] (::Franklin.var"#95#97"{Module,String,Array{Any,1},Int64})(::IOStream) at /home/rik/git/FR/src/eval/run.jl:67
+         [9] open(::Franklin.var"#95#97"{Module,String,Array{Any,1},Int64}, ::String, ::Vararg{String,N} where N; kwargs::Base.Iterators.Pairs{Union{},Union{},Tuple{},NamedTuple{(),Tuple{}}}) at ./io.jl:325
+        """
+    @test !occursin("redirect_stdout", F.trim_stacktrace(stacktrace))
+
+    # code with error
     c = """
         e = 0
         a = sqrt(-1)
@@ -74,6 +90,8 @@ end
     @test occursin("Warning: in <input string>", s)
     @test occursin("error of type 'DomainError'", s)
     @test occursin("Checking the output files", s)
+    @test occursin("throw_complex_domainerror(::Symbol, ::Float64) at", s)
+    @test !occursin("redirect_stdout", s)
 end
 
 @testset "i462" begin
diff --git a/test/latex/newcommand.jl b/test/latex/newcommand.jl
index 32dc504f..a3786da5 100644
--- a/test/latex/newcommand.jl
+++ b/test/latex/newcommand.jl
@@ -214,3 +214,11 @@ end
         \[\begin{array}{rcl} \sin^2(x)+\cos^2(x) &=& 1\end{array}\]
         """)
 end
+
+@testset "issue #720" begin
+    s = raw"""
+        \newcommand{\ip}[2]{\langle #1, #2 \rangle}
+        $$\ip{\dot{a}}{\dot{b}}$$
+        """ |> fd2html
+    @test isapproxstr(s, raw"\[\langle \dot{a}, \dot{b} \rangle\]")
+end
diff --git a/test/utils/warnings.jl b/test/utils/warnings.jl
index 561cce02..3c3796b5 100644
--- a/test/utils/warnings.jl
+++ b/test/utils/warnings.jl
@@ -26,7 +26,7 @@ end
     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("Warning: in <unknown>", s)
     @test occursin("An RSS description was found but without title", s)
     @test occursin("for page '/hey/ho/index.html'.", s)
 end