From 5eaac6ecf99535f8423147c6b6c336605afec40d Mon Sep 17 00:00:00 2001 From: Thibaut Lienart Date: Mon, 13 Apr 2020 10:16:52 +0200 Subject: [PATCH] v07 init (#443) --- .travis.yml | 2 - NEWS.md | 63 ++++++++++++++++++-- Project.toml | 18 +++--- README.md | 2 +- src/Franklin.jl | 9 ++- src/converter/html/blocks.jl | 98 +++++++++++++++++++------------- src/converter/html/functions.jl | 59 ++++++++++++------- src/converter/html/html.jl | 1 + src/converter/latex/hyperrefs.jl | 2 +- src/converter/latex/io.jl | 20 +++---- src/converter/latex/latex.jl | 13 ++++- src/converter/markdown/blocks.jl | 4 +- src/converter/markdown/md.jl | 51 ++++------------- src/converter/markdown/mddefs.jl | 39 +++++++++++++ src/converter/markdown/utils.jl | 9 ++- src/eval/module.jl | 6 +- src/manager/file_utils.jl | 67 +++++++++++++++++++--- src/manager/franklin.jl | 42 +++++++++----- src/parser/latex/blocks.jl | 31 +++++++--- src/parser/latex/tokens.jl | 23 ++++++-- src/parser/markdown/indent.jl | 87 ++++++++++++++-------------- src/parser/markdown/tokens.jl | 1 - src/parser/ocblocks.jl | 4 -- src/parser/tokens.jl | 6 ++ src/utils/misc.jl | 28 --------- src/utils/vars.jl | 68 ++++++++++++++++++++-- test/converter/markdown3.jl | 7 ++- test/coverage/extras1.jl | 1 - test/parser/2-blocks.jl | 2 +- test/parser/indentation++.jl | 63 ++++++++++++++++++++ test/runtests.jl | 13 ++++- test/templating/fill.jl | 34 +++++++++++ test/templating/for.jl | 42 ++++++++++++++ test/test_utils.jl | 9 ++- test/utils_file/basic.jl | 72 +++++++++++++++++++++++ 35 files changed, 735 insertions(+), 261 deletions(-) create mode 100644 src/converter/markdown/mddefs.jl create mode 100644 test/parser/indentation++.jl create mode 100644 test/templating/fill.jl create mode 100644 test/templating/for.jl create mode 100644 test/utils_file/basic.jl diff --git a/.travis.yml b/.travis.yml index 6f11424f6..98eacd811 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,6 @@ language: julia os: - linux julia: - - 1.1 - - 1.2 - 1.3 - 1.4 - nightly diff --git a/NEWS.md b/NEWS.md index 375bcc3f1..c9ca20d2d 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,12 +1,67 @@ # NEWS -This document keeps track of breaking changes and what you can do if you update and things don't work anymore. - -Notes are in reverse chronological order. +This document keeps track of **key new features**, **breaking changes** and what you can do if you update and things don't work anymore. You can also check out [this issue](https://github.com/tlienart/Franklin.jl/issues/323) with a more granular list of things that are being worked on / have been fixed / added. -### v0.6+ (new functionalities) +## v0.7 + +### Breaking changes + +* indented code blocks are now opt-in, prefer fenced code blocks with backticks `` ` ``. This helps avoid ambiguities in parsing. If you do want indented code blocks on a page use `@def indented_code = true`, if you want them everywhere, put that definition in your `config.md`. +* markers for comments now **must** be separated with a character distinct from `-` so `` is ok, `--->` is not. Rule of thumb: use a whitespace. + +**Note**: supported Julia versions: 1.3, **1.4**, 1.5. + +### New stuff + +The main new stuff are: + +* you can unpack in template for loops `{{for (x, y, z) in iterator}}`, +* you can use `{{var}}` as a short for `{{fill var}}`, you can use `{{...}}` blocks directly in markdown, +* variable definitions (`@def`) can now be multi-line, the secondary lines **must** be indented, +* you can access page variables from other pages using `{{fill var path}}` where `path` is the relative path to the other file so for instance `{{fill var index}}` or `{{fill var blog/page1}}`, this can be combined with a for loop for instance: + +``` +@def paths = ["index", "blog/page1"] +{{for p in paths}} + {{fill var p}} +{{end}} +``` + +And maybe most importantly, the addition of a `utils.jl` file to complement the `config.md`. In that file you can define variables and functions that can be used elsewhere on your site; they are evaluated in a `Utils` module which is imported in all evaluated code blocks. + +* if you define a variable `var = 5`, it will be accessible everywhere, taking priority over any other page variable (including global), you can call `{{fill var}}` or use it in an evaluated code block with `Utils.var`. +* if you define a function `foo() = print("hello")`, you can use it in an evaluated code block with `Utils.foo()` +* if you define a function `hfun_foo() = return "blah"`, you can use it as a template function `{{foo}}`, they have access to page variable names so this would also be valid: + +```julia +function hfun_bar(vname) # vname is a vector of string here + val = locvar(vname[1]) + return round(sqrt(val), digits=2) +end +``` + +which you can call with `{{bar var}}`. + +* if you define a function `lx_baz` you can use `\baz{...}` and have the function directly act on the input string, the syntax to use must conform to the following example: + +```julia +function lx_baz(com, _) + # keep this first line + brace_content = Franklin.content(com.braces[1]) # input string + # Now do whatever you want with the content: + return uppercase(brace_content) +end +``` + +which you can call with `\baz{some string}`. + + + +--- + +## v0.6+ * addition of an `ignore` global page variable to ignore files and directories, addition of a `div_content` page variable to change the name of the main container div. * Multiline markdown definitions are now allowed, the lines **must** be indented with four spaces e.g. this is ok: diff --git a/Project.toml b/Project.toml index a698183d5..2ecd1e4ca 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Franklin" uuid = "713c75ef-9fc9-4b05-94a9-213340da978e" authors = ["Thibaut Lienart "] -version = "0.6.17" +version = "0.7.0" [deps] Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" @@ -19,14 +19,14 @@ Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" [compat] -DocStringExtensions = "^0.8" -FranklinTemplates = "^0.5, 0.6" -HTTP = "^0.8" -Literate = "^2.2" -LiveServer = "^0.3" -NodeJS = "^0.6, ^1" -OrderedCollections = "^1.1" -julia = "^1.1" +DocStringExtensions = "0.8" +FranklinTemplates = "0.6" +HTTP = "0.8" +Literate = "2.2" +LiveServer = "0.3" +NodeJS = "0.6,1" +OrderedCollections = "1.1" +julia = "1.1" [extras] DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" diff --git a/README.md b/README.md index 3dc4a6ce0..6a4c0ca0f 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ See [the docs](https://franklinjl.org) for more information and examples. ## Getting started -With Julia ≥ 1.1: +With Julia ≥ 1.3: ```julia pkg> add Franklin diff --git a/src/Franklin.jl b/src/Franklin.jl index 81690db02..80935f693 100644 --- a/src/Franklin.jl +++ b/src/Franklin.jl @@ -1,6 +1,7 @@ module Franklin using FranklinTemplates +using FranklinTemplates: filecmp using Markdown using Markdown: htmlesc @@ -36,9 +37,10 @@ export jd2html # = fd2html # CONSTANTS # -# Obtained via `dig www...`; may change over time; see check_ping -# we check in sequence, one should work... this may need to be updated -# over time. +# 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` +# we check in sequence, one should work if the user is online... const IP_CHECK = ( "172.217.21.132" => "Google", # google "140.82.118.4" => "GitHub", # github @@ -139,6 +141,7 @@ include("eval/literate.jl") # > markdown include("converter/markdown/blocks.jl") include("converter/markdown/utils.jl") +include("converter/markdown/mddefs.jl") include("converter/markdown/md.jl") # > latex include("converter/latex/latex.jl") diff --git a/src/converter/html/blocks.jl b/src/converter/html/blocks.jl index a7937e8dd..80a78a7d5 100644 --- a/src/converter/html/blocks.jl +++ b/src/converter/html/blocks.jl @@ -157,56 +157,37 @@ end """ $SIGNATURES -Process a for block. +Process a for block (for a variable iterate). """ function process_html_for(hs::AS, qblocks::Vector{AbstractBlock}, i::Int)::Tuple{String,Int,Int} # check that the iterable is known β_open = qblocks[i] - vname = β_open.vname - iname = β_open.iname - if !haskey(LOCAL_VARS, iname) + vname = β_open.vname # x or (x, v) + iname = β_open.iname # var + + if iname ∉ UTILS_NAMES && !haskey(LOCAL_VARS, iname) throw(HTMLBlockError("The iterable '$iname' is not recognised. " * "Please make sure it's defined.")) end - - # try to close the for loop - if i == length(qblocks) - throw(HTMLBlockError("Could not close the conditional block " * - "starting with '$(qblocks[i].ss)'.")) - end - - init_idx = i - content = "" - - # inbalance keeps track of whether we've managed to find a - # matching {{end}}. It increases if it sees other opening {{if..}} - # and decreases if it sees a {{end}} - inbalance = 1 - while i < length(qblocks) && inbalance > 0 - i += 1 - inbalance += hbalance(qblocks[i]) - end - # we've exhausted the candidate qblocks and not found an appropriate {{end}} - if inbalance > 0 - throw(HTMLBlockError("Could not close the conditional block " * - "starting with '$(qblocks[init_idx].ss)'.")) + if iname ∈ UTILS_NAMES # can only happen if Utils is defined. + iter = getfield(Main.Utils, Symbol(iname)) + else + iter = locvar(iname) end - # we've found the closing {{end}} and index `i` - β_close = qblocks[i] - i_close = i - isempty(locvar(iname)) && @goto final_step + i_close, β_close = get_for_body(i, qblocks) + isempty(iter) && @goto final_step # is vname a single variable or multiple variables? # --> {{for v in iterate}} - # --> {{for (v1, v2) in iterate }} + # --> {{for (v1, v2) in iterate }} (unpacking) vnames = [vname] if startswith(vname, "(") vnames = strip.(split(vname[2:end-1], ",")) end # check that the first element of the iterate has the same length - el1 = first(locvar(iname)) + el1 = first(iter) length(vnames) in (1, length(el1)) || throw(HTMLBlockError("In a {{for ...}}, the first element of" * "the iterate has length $(length(el1)) but tried to unpack" * @@ -222,22 +203,57 @@ function process_html_for(hs::AS, qblocks::Vector{AbstractBlock}, isempty(strip(inner)) && @goto final_step content = "" if length(vnames) == 1 - for value in locvar(iname) + rx1 = Regex("{{\\s*fill\\s+$vname\\s*}}") # {{ fill v}} + rx2 = Regex("{{\\s*fill\\s+(\\S+)\\s+$vname\\s*}}") # {{ fill x v}} + for v in iter # at the moment we only consider {{fill ...}} - content *= replace(inner, - Regex("({{\\s*fill\\s+$vname\\s*}})") => "$value") + tmp = replace(inner, rx1 => "$v") + tmp = replace(tmp, rx2 => SubstitutionString("{{fill \\1 $v}}")) + content *= tmp end else - for value in locvar(iname) - temp = inner - for (vname, value) in zip(vnames, value) - temp = replace(temp, - Regex("({{\\s*fill\\s+$vname\\s*}})") => "$value") + for values in iter # each element of the iter can be unpacked + tmp = inner + for (vname, v) in zip(vnames, values) + rx1 = Regex("{{\\s*fill\\s+$vname\\s*}}") + rx2 = Regex("{{\\s*fill\\s+(\\S+)\\s+$vname\\s*}}") + tmp = replace(tmp, rx1 => "$v") + tmp = replace(tmp, rx2 => SubstitutionString("{{fill \\1 $v}}")) end - content *= temp + content *= tmp end end @label final_step head = nextind(hs, to(β_close)) return convert_html(content), head, i_close end + +""" +$SIGNATURES + +Extract the body of a for loop, keeping track of balancing. +""" +function get_for_body(i::Int, qblocks::Vector{AbstractBlock}) + # try to close the for loop + if i == length(qblocks) + throw(HTMLBlockError("Could not close the block starting with" * + "'$(qblocks[i].ss)'.")) + end + init_idx = i + content = "" + # inbalance keeps track of whether we've managed to find a + # matching {{end}}. It increases if it sees other opening {{if..}} + # and decreases if it sees a {{end}} + inb = 1 + while i < length(qblocks) && inb > 0 + i += 1 + inb += hbalance(qblocks[i]) + end + # we've exhausted the candidate qblocks and not found a matching {{end}} + if inb > 0 + throw(HTMLBlockError("Could not close the block starting with" * + "'$(qblocks[init_idx].ss)'.")) + end + # we've found the closing {{end}} and index `i` + return i, qblocks[i] +end diff --git a/src/converter/html/functions.jl b/src/converter/html/functions.jl index ea1c1743a..7feb8d37d 100644 --- a/src/converter/html/functions.jl +++ b/src/converter/html/functions.jl @@ -5,19 +5,20 @@ Helper function to process an individual block when it's a `HFun` such as `{{ fill author }}`. See also [`convert_html`](@ref). """ function convert_html_fblock(β::HFun)::String - # try to find a function `hfun_...` fun = Symbol("hfun_" * lowercase(β.fname)) - isdefined(Franklin, fun) && return eval(:($fun($β.params))) - - # if zero parameters, see if can fill - if isempty(β.params) && !isnothing(locvar(β.fname)) + ex = isempty(β.params) ? :($fun()) : :($fun($β.params)) + # see if a hfun was defined in utils + if isdefined(Main, :Utils) && isdefined(Main.Utils, fun) + res = Core.eval(Main.Utils, ex) + return string(res) + end + # see if a hfun was defined internally + isdefined(Franklin, fun) && return eval(ex) + # if zero parameters, see if can fill (case: {{vname}}) + if isempty(β.params) && + (!isnothing(locvar(β.fname)) || β.fname in UTILS_NAMES) return hfun_fill([β.fname]) end - - # XXX Future - # XXX see if there's an externally defined hfun - # XXX isdefined(Utils, fun) && Utils.eval(:($fun($β.params))) - # 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." @@ -29,24 +30,40 @@ end """ $(SIGNATURES) -H-Function of the form `{{ fill vname }}` to plug in the content of a -franklin-var `vname` (assuming it can be represented as a string). +H-Function of the form `{{ fill vname }}` or `{{ fill vname rpath}}` to plug in +the content of a franklin-var `vname` (assuming it can be represented as a +string). """ function hfun_fill(params::Vector{String})::String # check params - if length(params) != 1 - throw(HTMLFunctionError("I found a {{fill ...}} with more than one parameter. Verify.")) + if length(params) > 2 || isempty(params) + throw(HTMLFunctionError("{{fill ...}} should have one or two " * + "($(length(params)) given). Verify.")) end # form the fill repl = "" vname = params[1] - if haskey(LOCAL_VARS, vname) - # retrieve the value stored - tmp_repl = locvar(vname) - isnothing(tmp_repl) || (repl = string(tmp_repl)) - else - @warn "I found a '{{fill $vname}}' but I do not know the " * - "variable '$vname'. Ignoring." + if length(params) == 1 + if vname in UTILS_NAMES + repl = string(getfield(Main.Utils, Symbol(vname))) + else + tmp_repl = locvar(vname) + if isnothing(tmp_repl) + @warn "I found a '{{fill $vname}}' but I do not know the " * + "variable '$vname'. Ignoring." + else + repl = string(tmp_repl) + end + end + else # two parameters, look in a path + 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." + else + repl = string(tmp_repl) + end end return repl end diff --git a/src/converter/html/html.jl b/src/converter/html/html.jl index 6b31eeefc..bf9ded222 100644 --- a/src/converter/html/html.jl +++ b/src/converter/html/html.jl @@ -44,6 +44,7 @@ function fd2html_v(st::AS; internal::Bool=false, dir::String="")::Tuple{String,Dict} isempty(st) && return st if !internal + empty!(ALL_PAGE_VARS) FOLDER_PATH[] = isempty(dir) ? mktempdir() : dir set_paths!() def_GLOBAL_LXDEFS!() diff --git a/src/converter/latex/hyperrefs.jl b/src/converter/latex/hyperrefs.jl index f9a1bb815..88c226d4c 100644 --- a/src/converter/latex/hyperrefs.jl +++ b/src/converter/latex/hyperrefs.jl @@ -76,7 +76,7 @@ function lx_label(lxc::LxCom, _) end function lx_biblabel(lxc::LxCom, _)::String - name = refstring(strip(content(lxc.braces[1]))) + name = refstring(stent(lxc.braces[1])) PAGE_BIBREFS[name] = content(lxc.braces[2]) return "" end diff --git a/src/converter/latex/io.jl b/src/converter/latex/io.jl index 9b793a5b2..5634f0bb2 100644 --- a/src/converter/latex/io.jl +++ b/src/converter/latex/io.jl @@ -20,8 +20,8 @@ Different actions can be taken based on the qualifier: e.g.: `\\input{plot:4}{ex2}`. """ function lx_input(lxc::LxCom, _) - qualifier = lowercase(strip(content(lxc.braces[1]))) - rpath = strip(content(lxc.braces[2])) + qualifier = lowercase(stent(lxc.braces[1])) + rpath = stent(lxc.braces[2]) # check the qualifier if startswith(qualifier, "plot") # check if id is given e.g. \input{plot:5}{ex2} @@ -82,7 +82,7 @@ At most one will be true. See [`lx_show`](@ref). """ function lx_output(lxc::LxCom, lxd::Vector{LxDef}; reproc::Bool=false, res::Bool=false) - rpath = strip(content(lxc.braces[1])) + rpath = stent(lxc.braces[1]) cpaths = form_codepaths(rpath) outpath = cpaths.out_path respath = cpaths.res_path @@ -123,7 +123,7 @@ If you just want to include text as a plaintext block use `\\input{plaintext}{rpath}` instead. """ function lx_textinput(lxc::LxCom, lxd::Vector{LxDef}) - rpath = strip(content(lxc.braces[1])) + rpath = stent(lxc.braces[1]) input = "" try fp, = resolve_rpath(rpath) @@ -138,11 +138,11 @@ end Resolve a `\\figalt{alt}{rpath}` (find a fig and include it with alt). """ function lx_figalt(lxc::LxCom, _) - rpath = strip(content(lxc.braces[2])) - alt = strip(content(lxc.braces[1])) + rpath = stent(lxc.braces[2]) + alt = stent(lxc.braces[1]) path = parse_rpath(rpath; canonical=false, code=true) fdir, fext = splitext(path) - + # there are several cases # A. a path with no extension --> guess extension # B. a path with extension --> use that @@ -181,8 +181,8 @@ end Resolve a `\\tableinput{header}{rpath}` (find a table+header and include it). """ function lx_tableinput(lxc::LxCom, _) - rpath = strip(content(lxc.braces[2])) - header = strip(content(lxc.braces[1])) + rpath = stent(lxc.braces[2]) + header = stent(lxc.braces[1]) path = parse_rpath(rpath; canonical=false) fdir, fext = splitext(path) # copy-paste from resolve_lx_figalt() @@ -215,7 +215,7 @@ end Resolve a `\\literate{rpath}` (find a literate script and insert it). """ function lx_literate(lxc::LxCom, lxd::Vector{LxDef}) - rpath = strip(content(lxc.braces[1])) + rpath = stent(lxc.braces[1]) opath, haschanged = literate_to_franklin(rpath) # check file is there if isempty(opath) diff --git a/src/converter/latex/latex.jl b/src/converter/latex/latex.jl index c7a35c794..60b764908 100644 --- a/src/converter/latex/latex.jl +++ b/src/converter/latex/latex.jl @@ -10,7 +10,16 @@ function resolve_lxcom(lxc::LxCom, lxdefs::Vector{LxDef}; # retrieve the definition the command points to lxd = getdef(lxc) # it will be `nothing` in math mode, let KaTeX have it - lxd === nothing && return lxc.ss + if lxd === nothing + name = getname(lxc) # `\\cite` -> `cite` + fun = Symbol("lx_" * name) + @show fun + if isdefined(Main, :Utils) && isdefined(Main.Utils, fun) + return Core.eval(Main.Utils, :($fun($lxc, $lxdefs))) + else + return lxc.ss + end + end # otherwise it may be # -> empty, in which case try to find a specific internal definition or # return an empty string (see `commands.jl`) @@ -29,7 +38,7 @@ function resolve_lxcom(lxc::LxCom, lxdefs::Vector{LxDef}; # non-empty case, take the definition and iteratively replace any `#...` partial = lxd for (i, brace) in enumerate(lxc.braces) - cont = strip(content(brace)) + cont = stent(brace) # space-sensitive 'unsafe' one partial = replace(partial, "!#$i" => cont) # space-insensitive 'safe' one (e.g. `\mathbb#1`) diff --git a/src/converter/markdown/blocks.jl b/src/converter/markdown/blocks.jl index 3442c3ac6..74d9934a1 100644 --- a/src/converter/markdown/blocks.jl +++ b/src/converter/markdown/blocks.jl @@ -15,7 +15,7 @@ function convert_block(β::AbstractBlock, lxdefs::Vector{LxDef})::AS βn == :CODE_INLINE && return html_code_inline(content(β) |> htmlesc) βn == :CODE_BLOCK_LANG && return resolve_code_block(β.ss) βn == :CODE_BLOCK_IND && return convert_indented_code_block(β.ss) - βn == :CODE_BLOCK && return html_code(strip(content(β)), "{{fill lang}}") + βn == :CODE_BLOCK && return html_code(stent(β), "{{fill lang}}") βn == :ESCAPE && return chop(β.ss, head=3, tail=3) βn == :FOOTNOTE_REF && return convert_footnote_ref(β) @@ -30,7 +30,7 @@ function convert_block(β::AbstractBlock, lxdefs::Vector{LxDef})::AS # Div block --> need to process the block as a sub-element if βn == :DIV - raw_cont = strip(content(β)) + raw_cont = stent(β) cont = convert_md(raw_cont, lxdefs; isrecursive=true, has_mddefs=false) divname = chop(otok(β).ss, head=2, tail=0) diff --git a/src/converter/markdown/md.jl b/src/converter/markdown/md.jl index 41fe7915e..23022ae60 100644 --- a/src/converter/markdown/md.jl +++ b/src/converter/markdown/md.jl @@ -25,7 +25,9 @@ function convert_md(mds::AS, isrecursive::Bool=false, isinternal::Bool=false, isconfig::Bool=false, - has_mddefs::Bool=true )::String + has_mddefs::Bool=true, + only_mddefs::Bool=false, # see pagevar + )::String # instantiate page dictionaries isrecursive || isinternal || set_page_env() # if we're given a substring, force it to a string @@ -99,6 +101,8 @@ function convert_md(mds::AS, if has_mddefs process_mddefs(blocks, isconfig) end + #> 4.0 if only_mddefs, terminate early, we don't care about the rest + only_mddefs && return "" #> 4.b if config, update global lxdefs as well if isconfig for lxd ∈ lxdefs @@ -121,6 +125,11 @@ function convert_md(mds::AS, # and add them to the blocks to insert fnrefs = filter(τ -> τ.name == :FOOTNOTE_REF, tokens) + # Discard indented blocks unless locvar("indented_code") + if !locvar("indented_code") + filter!(b -> b.name != :CODE_BLOCK_IND, blocks) + end + #> 1. Merge all the blocks that will need further processing before # insertion b2insert = merge_blocks(lxcoms, deactivate_divs(blocks), @@ -366,43 +375,3 @@ function convert_inter_html(ihtml::AS, # return the full string return String(take!(htmls)) end - - -""" -$(SIGNATURES) - -Convenience function to process markdown definitions `@def ...` as appropriate. -Depending on `isconfig`, will update `GLOBAL_VARS` or `LOCAL_VARS`. - -**Arguments** - -* `blocks`: vector of active docs -* `isconfig`: whether the file being processed is the config file - (--> global page variables) -""" -function process_mddefs(blocks::Vector{OCBlock}, isconfig::Bool)::Nothing - # Find all markdown definitions (MD_DEF) blocks - mddefs = filter(β -> (β.name == :MD_DEF), blocks) - # empty container for the assignments - assignments = Vector{Pair{String, String}}() - # go over the blocks, and extract the assignment - for (i, mdd) ∈ enumerate(mddefs) - inner = strip(content(mdd)) - m = match(ASSIGN_PAT, inner) - if isnothing(m) - @warn "Found delimiters for an @def environment but it didn't " * - "have the right @def var = ... format. Verify (ignoring " * - "for now)." - continue - end - vname, vdef = m.captures[1:2] - push!(assignments, (String(vname) => String(vdef))) - end - # if in config file, update `GLOBAL_VARS` and `GLOBAL_LXDEFS` - if isconfig - set_vars!(GLOBAL_VARS, assignments) - else - set_vars!(LOCAL_VARS, assignments) - end - return nothing -end diff --git a/src/converter/markdown/mddefs.jl b/src/converter/markdown/mddefs.jl new file mode 100644 index 000000000..b14ad53a1 --- /dev/null +++ b/src/converter/markdown/mddefs.jl @@ -0,0 +1,39 @@ +""" +$(SIGNATURES) + +Convenience function to process markdown definitions `@def ...` as appropriate. +Depending on `isconfig`, will update `GLOBAL_VARS` or `LOCAL_VARS`. + +**Arguments** + +* `blocks`: vector of active docs +* `isconfig`: whether the file being processed is the config file + (--> global page variables) +""" +function process_mddefs(blocks::Vector{OCBlock}, isconfig::Bool)::Nothing + # Find all markdown definitions (MD_DEF) blocks + mddefs = filter(β -> (β.name == :MD_DEF), blocks) + # empty container for the assignments + assignments = Vector{Pair{String, String}}() + # go over the blocks, and extract the assignment + for (i, mdd) ∈ enumerate(mddefs) + 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)." + continue + end + vname, vdef = m.captures[1:2] + push!(assignments, (String(vname) => String(vdef))) + end + # if in config file, update `GLOBAL_VARS` and `GLOBAL_LXDEFS` + if isconfig + set_vars!(GLOBAL_VARS, assignments) + else + set_vars!(LOCAL_VARS, assignments) + ALL_PAGE_VARS[splitext(locvar("fd_rpath"))[1]] = deepcopy(LOCAL_VARS) + end + return nothing +end diff --git a/src/converter/markdown/utils.jl b/src/converter/markdown/utils.jl index 62a6f5657..b495a9a43 100644 --- a/src/converter/markdown/utils.jl +++ b/src/converter/markdown/utils.jl @@ -10,7 +10,14 @@ function md2html(ss::AS; stripp::Bool=false)::AS # if there's nothing, return that... isempty(ss) && return ss # Use Julia's Markdown parser followed by Julia's MD->HTML conversion - partial = ss |> fix_inserts |> Markdown.parse |> Markdown.html + partial = ss |> fix_inserts |> Markdown.parse + # take over from the parsing of indented blocks + for (i, c) in enumerate(partial.content) + c isa Markdown.Code || continue + partial.content[i] = Markdown.Paragraph(Any[c.code]) + end + # Use Julia's MD->HTML conversion + partial = partial |> Markdown.html # Markdown.html transforms {{ with HTML entities but we don't want that partial = replace(partial, r"{{" => "{{") partial = replace(partial, r"}}" => "}}") diff --git a/src/eval/module.jl b/src/eval/module.jl index 459f7d68b..c4944f68d 100644 --- a/src/eval/module.jl +++ b/src/eval/module.jl @@ -36,7 +36,11 @@ function newmodule(name::String)::Module redirect_stderr(outf) do mod = Core.eval(Main, Meta.parse(""" module $name - import Franklin: @OUTPUT, fdplotly, locvar + import Franklin + import Franklin: @OUTPUT, fdplotly, locvar, pagevar + if isdefined(Main, :Utils) && typeof(Main.Utils) == Module + import ..Utils + end end """)) end diff --git a/src/manager/file_utils.jl b/src/manager/file_utils.jl index 50cfdc369..2c2c8d8f6 100644 --- a/src/manager/file_utils.jl +++ b/src/manager/file_utils.jl @@ -1,9 +1,9 @@ """ $(SIGNATURES) -Checks for a `config.md` file in `PATHS[:src]` and uses it to set the global -variables referenced in `GLOBAL_VARS` it also sets the global latex commands -via `GLOBAL_LXDEFS`. If the configuration file is not found a warning is shown. +Checks for a `config.md` file and uses it to set the global variables +referenced in `GLOBAL_VARS` it also sets the global latex commands via +`GLOBAL_LXDEFS`. If the configuration file is not found a warning is shown. The keyword `init` is used internally to distinguish between the first call where only structural variables are considered (e.g. controlling folder structure). @@ -34,6 +34,36 @@ function process_config(; init::Bool=false)::Nothing return nothing end +""" +$(SIGNATURES) + +Checks for a `utils.jl` file and uses it to set global computed variables, +functions and html functions. Whatever is defined in `utils.jl` takes +precedence over things defined internally in Franklin or in the global vars; +in particular users can redefine the behaviour of `hfuns` though that's not +recommended. +""" +function process_utils() + utils_path_v1 = joinpath(FOLDER_PATH[], "src", "utils.jl") + utils_path_v2 = joinpath(FOLDER_PATH[], "utils.jl") + if isfile(utils_path_v2) + utils = utils_path_v2 + elseif isfile(utils_path_v1) + utils = utils_path_v1 + else + return nothing + end + # wipe / create module Utils + newmodule("Utils") + Core.eval(Main.Utils, :(include($utils))) + # # keep track of utils names + ns = String.(names(Main.Utils, all=true)) + filter!(n -> n[1] != '#' && n ∉ ("eval", "include", "Utils"), ns) + empty!(UTILS_NAMES) + append!(UTILS_NAMES, ns) + return nothing +end + """ $(SIGNATURES) @@ -199,10 +229,33 @@ Convenience function to assemble the html out of its parts. build_page(head::String, content::String, pgfoot::String, foot::String) = "$head\n
\n$content\n$pgfoot\n
\n$foot" -function set_cur_rpath(fpath::String) + +""" + get_rpath(fpath) + +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`. +""" +function get_rpath(fpath::String) root = path(ifelse(FD_ENV[:STRUCTURE] < v"0.2", :src, :folder)) - cur_rpath = fpath[lastindex(root)+length(PATH_SEP)+1:end] - FD_ENV[:CUR_PATH] = cur_rpath - set_var!(LOCAL_VARS, "fd_rpath", FD_ENV[:CUR_PATH]) + return fpath[lastindex(root)+length(PATH_SEP)+1:end] +end + +""" + set_cur_rpath(fpath) + +Takes the path to the current file and sets the `fd_rpath` local page variable +as well as the `FD_ENV[:CUR_PATH]` variable (used for conditional blocks +depending on URL for instance). +""" +function set_cur_rpath(fpath::String; isrelative::Bool=false) + if isrelative + rpath = fpath + else + rpath = get_rpath(fpath) + end + FD_ENV[:CUR_PATH] = rpath + set_var!(LOCAL_VARS, "fd_rpath", rpath) return nothing end diff --git a/src/manager/franklin.jl b/src/manager/franklin.jl index 390d75217..9d746e846 100644 --- a/src/manager/franklin.jl +++ b/src/manager/franklin.jl @@ -3,7 +3,7 @@ $SIGNATURES Clear the environment dictionaries. """ -clear_dicts() = empty!.((GLOBAL_LXDEFS, GLOBAL_VARS, LOCAL_VARS)) +clear_dicts() = empty!.((GLOBAL_LXDEFS, GLOBAL_VARS, LOCAL_VARS, ALL_PAGE_VARS)) """ $(SIGNATURES) @@ -14,18 +14,24 @@ Keyword arguments: * `clear=false`: whether to remove any existing output directory * `verb=false`: whether to display messages -* `port=8000`: the port to use for the local server (should pick a number between 8000 and 9000) +* `port=8000`: the port to use for the local server (should pick a number + between 8000 and 9000) * `single=false`: whether to run a single pass or run continuously * `prerender=false`: whether to pre-render javascript (KaTeX and highlight.js) * `nomess=false`: suppresses all messages (internal use). -* `isoptim=false`: whether we're in an optimisation phase or not (if so, links are fixed in case - of a project website, see [`write_page`](@ref). -* `no_fail_prerender=true`: whether, in a prerendering phase, ignore errors and try to produce an output +* `isoptim=false`: whether we're in an optimisation phase or not (if so, + links are fixed in case of a project website, see + [`write_page`](@ref). +* `no_fail_prerender=true`: whether, in a prerendering phase, ignore errors and + try to produce an output * `eval_all=false`: whether to force re-evaluation of all code blocks -* `silent=false`: switch this on to suppress all output (including eval statements). -* `cleanup=true`: whether to clear environment dictionaries, see [`cleanup`](@ref). -* `on_write(pg, fd_vars)`: callback function after the page is rendered, passing as arguments - the rendered page and the page variables +* `silent=false`: switch this on to suppress all output (including eval + statements). +* `cleanup=true`: whether to clear environment dictionaries, see + [`cleanup`](@ref). +* `on_write(pg, fd_vars)`: callback function after the page is rendered, + passing as arguments the rendered page and the page + variables """ function serve(; clear::Bool=false, verb::Bool=false, @@ -143,8 +149,8 @@ function fd_setup(; clear::Bool=true)::NamedTuple prepare_output_dir(clear) # . recovering the list of files in the input dir we care about - # -- these are stored in dictionaries, the key is the full path and the value is the time of - # last change (useful for continuous monitoring) + # -- these are stored in dictionaries, the key is the full path and the + # value is the time of last change (useful for continuous monitoring) md_pages = TrackedFiles() html_pages = TrackedFiles() other_files = TrackedFiles() @@ -195,6 +201,7 @@ function fd_fullpass(watched_files::NamedTuple; clear::Bool=false, # process configuration file (see also `process_mddefs!`) process_config() + process_utils() # form page segments root_key = ifelse(FD_ENV[:STRUCTURE] < v"0.2", :src, :folder) @@ -263,7 +270,12 @@ function fd_loop(cycle_counter::Int, ::LiveServer.FileWatcher, # anything, we just remove the file reference from the corresponding # dictionary. for d ∈ watched_files, (fpair, _) ∈ d - isfile(joinpath(fpair...)) || delete!(d, fpair) + fpath = joinpath(fpair...) + if !isfile(fpath) + delete!(d, fpair) + rp = splitext(get_rpath(fpath))[1] + haskey(ALL_PAGE_VARS, rp) && delete!(ALL_PAGE_VARS, rp) + end end # 2) scan the input folder, if new files have been added then this will # update the dictionaries @@ -280,10 +292,14 @@ function fd_loop(cycle_counter::Int, ::LiveServer.FileWatcher, cur_t <= t && continue # if there was then the file has been modified and should be # re-processed + copied - fmsg = rpad("→ file $(fpath[length(FOLDER_PATH[])+1:end]) was modified ", 30) + fmsg = rpad("→ file $(fpath[length(FOLDER_PATH[])+1:end]) was " * + "modified ", 30) verb && print(fmsg) dict[fpair] = cur_t + # Reprocess utils.jl in Utils module + process_utils() + # if it's an infra_file trigger a fullpass as potentially # the whole website depends upon it (e.g. CSS) if haskey(watched_files[:infra], fpair) diff --git a/src/parser/latex/blocks.jl b/src/parser/latex/blocks.jl index 22b416ed1..b65cdfd9a 100644 --- a/src/parser/latex/blocks.jl +++ b/src/parser/latex/blocks.jl @@ -74,7 +74,7 @@ function find_lxdefs(tokens::Vector{Token}, blocks::Vector{OCBlock}) # keep track of the command name, definition and where it stops lxname = matched.captures[1] - lxdef = strip(content(defining_braces)) + lxdef = stent(defining_braces) todef = to(defining_braces) # store the new latex command push!(lxdefs, LxDef(lxname, lxnarg, lxdef, fromτ, todef)) @@ -119,7 +119,9 @@ function get_lxdef_ref(lxname::SubString, lxdefs::Vector{LxDef}, fromlx = from(lxname) + offset filter!(k -> (fromlx > from(lxdefs[k])), ks) if isempty(ks) - if !inmath + if isdefined(Main, :Utils) && isdefined(Main.Utils, Symbol("lx_$(lxname[2:end])")) + return Ref(nothing) + elseif !inmath throw(LxComError("Command '$lxname' was used before it was defined.")) end # not found but inmath --> let KaTex deal with it @@ -149,11 +151,22 @@ function find_lxcoms(tokens::Vector{Token}, lxdefs::Vector{LxDef}, # 1. look for the definition given the command name lxname = τ.ss lxdefref = get_lxdef_ref(lxname, lxdefs, inmath, offset) + derived = false # will only be nothing in a 'inmath' --> no failure, just ignore token - isnothing(lxdefref[]) && continue - - # 2. retrieve number of arguments - lxnarg = getindex(lxdefref).narg + if isnothing(lxdefref[]) + lxn = lxname[2:end] + fun = Symbol("lx_" * lxn) + if isdefined(Main, :Utils) && isdefined(Main.Utils, fun) + # take a single bracket + lxnarg = 1 + derived = true + else + continue + end + else + lxnarg = getindex(lxdefref).narg + end + # 2. explore with # arguments # 2.a there are none if lxnarg == 0 push!(lxcoms, LxCom(lxname, lxdefref)) @@ -188,7 +201,11 @@ function find_lxcoms(tokens::Vector{Token}, lxdefs::Vector{LxDef}, from_c = from(τ) to_c = to(cand_braces[end]) str_c = subs(str(τ), from_c, to_c) - push!(lxcoms, LxCom(str_c, lxdefref, cand_braces)) + if derived + push!(lxcoms, LxCom(str_c, nothing, cand_braces)) + else + push!(lxcoms, LxCom(str_c, lxdefref, cand_braces)) + end # deactivate tokens in the span of the command (will be # reparsed later) diff --git a/src/parser/latex/tokens.jl b/src/parser/latex/tokens.jl index 6fe2e5d94..635c2b541 100644 --- a/src/parser/latex/tokens.jl +++ b/src/parser/latex/tokens.jl @@ -45,9 +45,9 @@ $(TYPEDEF) A `LxCom` has a similar content as a `Block`, with the addition of the definition and a vector of brace blocks. """ struct LxCom <: AbstractBlock - ss ::SubString # \\command - lxdef ::Ref{LxDef} # definition of the command - braces::Vector{OCBlock} # relevant {...} associated with the command + ss ::SubString # \\command + lxdef ::Union{Nothing,Ref{LxDef}} # definition of the command + braces::Vector{OCBlock} # relevant {...} with the command end LxCom(ss, def) = LxCom(ss, def, Vector{OCBlock}()) from(lxc::LxCom) = from(lxc.ss) @@ -58,10 +58,23 @@ to(lxc::LxCom) = to(lxc.ss) For a given `LxCom`, retrieve the definition attached to the corresponding `LxDef` via the reference. """ -getdef(lxc::LxCom)::AS = getindex(lxc.lxdef).def +function getdef(lxc::LxCom)::Union{Nothing,AS} + if isnothing(lxc.lxdef) + return nothing + end + return getindex(lxc.lxdef).def +end """ For a given `LxCom`, retrieve the name of the command via the reference. Example: `\\cite` --> `cite`. """ -getname(lxc::LxCom)::String = String(getindex(lxc.lxdef).name)[2:end] +function getname(lxc::LxCom)::String + if isnothing(lxc.lxdef) + s = String(lxc.ss) + j = findfirst('{', s) + return lxc.ss[2:prevind(s, j)] + end + return String(getindex(lxc.lxdef).name)[2:end] +end +# XXX if nothing lxc.lxdef then extract from lxc.ss but extract before {} diff --git a/src/parser/markdown/indent.jl b/src/parser/markdown/indent.jl index ba70063b8..bc5ec724e 100644 --- a/src/parser/markdown/indent.jl +++ b/src/parser/markdown/indent.jl @@ -1,49 +1,6 @@ """ $(SIGNATURES) -In initial phase, discard all `:LR_INDENT` that don't meet the requirements for -an indented code block. I.e.: where the first line is not preceded by a blank -line and then subsequently followed by indented lines. -""" -function filter_lr_indent!(vt::Vector{Token}, s::String)::Nothing - tind_idx = [i for i in eachindex(vt) if vt[i].name == :LR_INDENT] - isempty(tind_idx) && return nothing - disable = zeros(Bool, length(tind_idx)) - open = false - i = 1 - while i <= length(tind_idx) - t = vt[tind_idx[i]] - if !open - # check if previous line is blank - prev_idx = prevind(s, from(t)) - if prev_idx < 1 || s[prev_idx] != '\n' - disable[i] = true - else - open = true - end - i += 1 - else - # if touching previous line, keep open and go to next - prev = vt[tind_idx[i-1]] - if prev.lno + 1 == t.lno - i += 1 - else - # otherwise close, and continue (will go to !open) - open = false - continue - end - end - end - for i in eachindex(disable) - disable[i] || continue - vt[tind_idx[i]] = Token(:LINE_RETURN, subs(vt[tind_idx[i]].ss, 1)) - end - return nothing -end - -""" -$(SIGNATURES) - Find markers for indented lines (i.e. a line return followed by a tab or 4 spaces). """ @@ -86,6 +43,48 @@ function find_indented_blocks!(tokens::Vector{Token}, st::String)::Nothing return nothing end +""" +$(SIGNATURES) + +In initial phase, discard all `:LR_INDENT` that don't meet the requirements for +an indented code block. I.e.: where the first line is not preceded by a blank +line and then subsequently followed by indented lines. +""" +function filter_lr_indent!(vt::Vector{Token}, s::String)::Nothing + tind_idx = [i for i in eachindex(vt) if vt[i].name == :LR_INDENT] + isempty(tind_idx) && return nothing + disable = zeros(Bool, length(tind_idx)) + open = false + i = 1 + while i <= length(tind_idx) + t = vt[tind_idx[i]] + if !open + # check if previous line is blank + prev_idx = prevind(s, from(t)) + if prev_idx < 1 || s[prev_idx] != '\n' + disable[i] = true + else + open = true + end + i += 1 + else + # if touching previous line, keep open and go to next + prev = vt[tind_idx[i-1]] + if prev.lno + 1 == t.lno + i += 1 + else + # otherwise close, and continue (will go to !open) + open = false + continue + end + end + end + for i in eachindex(disable) + disable[i] || continue + vt[tind_idx[i]] = Token(:LINE_RETURN, subs(vt[tind_idx[i]].ss, 1)) + end + return nothing +end """ $SIGNATURES @@ -145,7 +144,7 @@ $SIGNATURES Discard any indented block that is within a larger block to avoid ambiguities (see #285). """ -function filter_indented_blocks!(blocks::Vector{OCBlock})::Nothing\ +function filter_indented_blocks!(blocks::Vector{OCBlock})::Nothing # retrieve the indices of the indented blocks idx = [i for i in eachindex(blocks) if blocks[i].name == :CODE_BLOCK_IND] isempty(idx) && return nothing diff --git a/src/parser/markdown/tokens.jl b/src/parser/markdown/tokens.jl index bb4d34f3c..12a9b63ee 100644 --- a/src/parser/markdown/tokens.jl +++ b/src/parser/markdown/tokens.jl @@ -21,7 +21,6 @@ const MD_1C_TOKENS_LX = LittleDict{Char, Symbol}( '}' => :LXB_CLOSE ) - """ MD_TOKENS diff --git a/src/parser/ocblocks.jl b/src/parser/ocblocks.jl index 21fcee046..bc4ab4d3c 100644 --- a/src/parser/ocblocks.jl +++ b/src/parser/ocblocks.jl @@ -77,12 +77,8 @@ vector of vectors of ocblocks. """ function find_all_ocblocks(tokens::Vector{Token}, ocplist::Vector{OCProto}; inmath=false) - ocbs_all = Vector{OCBlock}() for ocp ∈ ocplist - if ocp.name == :CODE_BLOCK_IND && !locvar("indented_code") - continue - end ocbs, tokens = find_ocblocks(tokens, ocp; inmath=inmath) append!(ocbs_all, ocbs) end diff --git a/src/parser/tokens.jl b/src/parser/tokens.jl index ae0d75abc..0eafec0cb 100644 --- a/src/parser/tokens.jl +++ b/src/parser/tokens.jl @@ -124,6 +124,12 @@ function content(ocb::OCBlock)::SubString return subs(s, cfrom, cto) end +""" +$(SIGNATURES) + +Convenience function to strip the content of a block. +""" +stent = strip ∘ content #= FUNCTIONS / CONSTANTS THAT HELP DEFINE TOKENS diff --git a/src/utils/misc.jl b/src/utils/misc.jl index a66cf5b75..d28fe0a25 100644 --- a/src/utils/misc.jl +++ b/src/utils/misc.jl @@ -222,34 +222,6 @@ function rprint(s::AS)::Nothing return nothing end -""" - filecmp(path1, path2) - -Takes 2 absolute paths and check if the files are different. -This code was suggested by Steven J. Johnson on discourse: -https://discourse.julialang.org/t/how-to-obtain-the-result-of-a-diff-between-2-files-in-a-loop/23784/4 -""" -function filecmp(path1::AbstractString, path2::AbstractString) - stat1, stat2 = stat(path1), stat(path2) - if !(isfile(stat1) && isfile(stat2)) || filesize(stat1) != filesize(stat2) - return false - end - stat1 == stat2 && return true # same file - open(path1, "r") do file1 - open(path2, "r") do file2 - buf1 = Vector{UInt8}(undef, 32768) - buf2 = similar(buf1) - while !eof(file1) && !eof(file2) - n1 = readbytes!(file1, buf1) - n2 = readbytes!(file2, buf2) - n1 != n2 && return false - 0 != Base._memcmp(buf1, buf2, n1) && return false - end - return eof(file1) == eof(file2) - end - end -end - """ check_ping(ipaddr) diff --git a/src/utils/vars.jl b/src/utils/vars.jl index d452b2306..301c855dc 100644 --- a/src/utils/vars.jl +++ b/src/utils/vars.jl @@ -55,7 +55,7 @@ const LOCAL_VARS_DEFAULT = [ "date" => Pair(Date(1), (String, Date, Nothing)), "lang" => Pair("julia", (String,)), # default lang indented code "reflinks" => Pair(true, (Bool,)), # are there reflinks? - "indented_code" => Pair(true, (Bool,)), # support indented code? + "indented_code" => Pair(false, (Bool,)), # support indented code? # ----------------- # TABLE OF CONTENTS "mintoclevel" => Pair(1, (Int,)), # set to 2 to ignore h1 @@ -116,16 +116,72 @@ function def_LOCAL_VARS!()::Nothing end """ + locvar(name) + Convenience function to get the value associated with a local var. Return `nothing` if the variable is not found. """ -locvar(name::String) = haskey(LOCAL_VARS, name) ? LOCAL_VARS[name].first : nothing +function locvar(name::Union{Symbol,String}) + name = String(name) + return haskey(LOCAL_VARS, name) ? LOCAL_VARS[name].first : nothing +end """ + globvar(name) + Convenience function to get the value associated with a global var. Return `nothing` if the variable is not found. """ -globvar(name::String) = haskey(GLOBAL_VARS, name) ? GLOBAL_VARS[name].first : nothing +function globvar(name::Union{Symbol,String}) + name = String(name) + return haskey(GLOBAL_VARS, name) ? GLOBAL_VARS[name].first : nothing +end + + +""" +Dict to keep track of all pages and their vars. Each key is a relative path +to a page, values are PageVars. +""" +const ALL_PAGE_VARS = Dict{String,PageVars}() + +""" + pagevar(rpath, name) + +Convenience function to get the value associated with a var available to a page +corresponding to `rpath`. So for instance if `blog/index.md` has `@def var = 0` +then this can be accessed with `pagevar("var", "blog/index")`. +If `rpath` is not yet a key of `ALL_PAGE_VARS` then maybe the page hasn't been +processed yet so force a pass over that page. +""" +function pagevar(rpath::AS, name::Union{Symbol,String}) + rpath = splitext(rpath)[1] + if !haskey(ALL_PAGE_VARS, rpath) + # does there exist a file with a `.md` ? if so go over it + # otherwise return nothing + fpath = rpath * ".md" + candpath = FD_ENV[:STRUCTURE] < v"0.2" ? + joinpath(path(:src), fpath) : + joinpath(path(:folder), fpath) + isfile(candpath) || return nothing + # store curpath + bk_path = locvar("fd_rpath") + # set temporary cur path (so that defs go to the right place) + set_cur_rpath(rpath, isrelative=true) + # effectively we only care about the mddefs + convert_md(read(fpath, String), only_mddefs=true, isinternal=true) + # re-set the cur path to what it was before + set_cur_rpath(bk_path) + end + name = String(name) + haskey(ALL_PAGE_VARS[rpath], name) || return nothing + return ALL_PAGE_VARS[rpath][name].first +end + +""" +Keep track of the names declared in the Utils module. +""" +const UTILS_NAMES = Vector{String}() + """ Keep track of seen headers. The key is the refstring, the value contains the @@ -187,8 +243,8 @@ Convenience functions related to the page vars $(SIGNATURES) Convenience function taking a `DateTime` object and returning the corresponding -formatted string with the format contained in `LOCAL_VARS["date_format"]` and -with the locale data provided in `date_months`, `date_shortmonths`, `date_days`, +formatted string with the format contained in `LOCAL_VARS["date_format"]` and +with the locale data provided in `date_months`, `date_shortmonths`, `date_days`, and `date_shortdays` local variables. If `short` variations are not provided, automatically construct them using the first three letters of the names in `date_months` and `date_days`. @@ -214,7 +270,7 @@ function fd_date(d::DateTime) shortmonths = first.(months, 3) end # set locale for this page - Dates.LOCALES["date_locale"] = Dates.DateLocale(months, shortmonths, + Dates.LOCALES["date_locale"] = Dates.DateLocale(months, shortmonths, days, shortdays) return Dates.format(d, format, locale="date_locale") end diff --git a/test/converter/markdown3.jl b/test/converter/markdown3.jl index 128f85df7..0651b61c8 100644 --- a/test/converter/markdown3.jl +++ b/test/converter/markdown3.jl @@ -100,10 +100,10 @@ end st = raw"""A `single` and ``double ` double`` B""" steps = explore_md_steps(st) blocks, tokens = steps[:ocblocks] + @test blocks[2].name == :CODE_INLINE + @test F.content(blocks[2]) == "double ` double" @test blocks[1].name == :CODE_INLINE @test F.content(blocks[1]) == "single" - @test F.content(blocks[2]) == "double ` double" - @test blocks[2].name == :CODE_INLINE st = raw"""A `single` and ``double ` double`` and ``` triple ``` B""" steps = explore_md_steps(st) @@ -225,6 +225,7 @@ end @testset "IndCode" begin # issue 207 st = raw""" + @def indented_code = true A a = 1+1 @@ -248,6 +249,7 @@ end

""") st = raw""" + @def indented_code = true A `single` and ```python blah``` and a = 1+1 @@ -276,6 +278,7 @@ end """) st = raw""" + @def indented_code = true A function foo() diff --git a/test/coverage/extras1.jl b/test/coverage/extras1.jl index 68ee05185..9cde34d4e 100644 --- a/test/coverage/extras1.jl +++ b/test/coverage/extras1.jl @@ -5,7 +5,6 @@ end @testset "Conv-html" begin - @test_throws F.HTMLFunctionError F.convert_html("{{fill bb cc}}") @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}}")) == "??" diff --git a/test/parser/2-blocks.jl b/test/parser/2-blocks.jl index 714907ae7..573dc05ba 100644 --- a/test/parser/2-blocks.jl +++ b/test/parser/2-blocks.jl @@ -4,7 +4,7 @@ vh = s -> (t = vfn(s); F.validate_headers!(t); t) fib = s -> (t = vh(s); F.find_indented_blocks!(t, s); t) fib2 = s -> (t = fib(s); F.filter_lr_indent!(t, s); t) -blk = s -> (F.def_LOCAL_VARS!(); F.find_all_ocblocks(fib2(s), F.MD_OCB_ALL)) +blk = s -> (F.def_LOCAL_VARS!(); F.set_var!(F.LOCAL_VARS, "indented_code", true); F.find_all_ocblocks(fib2(s), F.MD_OCB_ALL)) blk2 = s -> ((b, t) = blk(s); F.merge_indented_blocks!(b, s); b) blk3 = s -> (b = blk2(s); F.filter_indented_blocks!(b); b) diff --git a/test/parser/indentation++.jl b/test/parser/indentation++.jl new file mode 100644 index 000000000..0a303d117 --- /dev/null +++ b/test/parser/indentation++.jl @@ -0,0 +1,63 @@ +@testset "indentation" begin + mds = """ + A + B + C + D""" + + tokens = F.find_tokens(mds, F.MD_TOKENS, F.MD_1C_TOKENS) + F.find_indented_blocks!(tokens, mds) + toks = deepcopy(tokens) + + @test tokens[1].name == :LR_INDENT + @test tokens[2].name == :LR_INDENT + @test tokens[3].name == :LINE_RETURN + + ocp = F.OCProto(:CODE_BLOCK_IND, :LR_INDENT, (:LINE_RETURN,), false) + + blocks, tokens = F.find_ocblocks(tokens, ocp) + + @test blocks[1].name == :CODE_BLOCK_IND + @test F.content(blocks[1]) == "B\n C" + + blocks, tokens = F.find_all_ocblocks(toks, [ocp]) + + @test blocks[1].name == :CODE_BLOCK_IND + @test F.content(blocks[1]) == "B\n C" + + mds = """ + @def indented_code = true + A + + B + C + D""" + steps = explore_md_steps(mds) + toks = steps[:tokenization].tokens + @test toks[4].name == toks[5].name == :LR_INDENT + blk = steps[:ocblocks].blocks + @test blk[2].name == :CODE_BLOCK_IND + b2i = steps[:b2insert].b2insert + @test b2i[2].name == :CODE_BLOCK_IND + @test isapproxstr(mds |> fd2html_td, """ +

A

B
+        C
D

+ """) +end + +@testset "ind+lx" begin + s = raw""" + \newcommand{\julia}[1]{ + ```julia + #1 + ``` + } + Hello + \julia{a=5 + x=3} + """ |> fd2html_td + @test isapproxstr(s, """ +

Hello

a=5
+        x=3

+ """) +end diff --git a/test/runtests.jl b/test/runtests.jl index 5d26305d9..6848f0cf8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -7,9 +7,11 @@ F.FD_ENV[:SILENT_MODE] = true F.FD_ENV[:STRUCTURE] = v"0.1" # legacy, it's switched up in the tests. # UTILS -println("UTILS") +println("UTILS-1") include("utils/folder_structure.jl") include("utils/paths_vars.jl"); include("test_utils.jl") +# --- +println("UTILS-2") include("utils/misc.jl") include("utils/errors.jl") include("utils/html.jl") @@ -35,6 +37,7 @@ include("parser/markdown+latex.jl") include("parser/markdown-extra.jl") include("parser/footnotes+links.jl") include("parser/latex++.jl") +include("parser/indentation++.jl") println("🍺") # EVAL @@ -97,6 +100,14 @@ begin end cd(dirname(dirname(pathof(Franklin)))) +println("TEMPLATING") +include("templating/for.jl") +# XXX XXX +include("templating/fill.jl") + +println("UTILS FILE") +include("utils_file/basic.jl") + println("INTEGRATION") include("integration/literate.jl") include("integration/literate_fs2.jl") diff --git a/test/templating/fill.jl b/test/templating/fill.jl new file mode 100644 index 000000000..39289e68d --- /dev/null +++ b/test/templating/fill.jl @@ -0,0 +1,34 @@ +fs2() +write(joinpath("_layout", "head.html"), "") +write(joinpath("_layout", "foot.html"), "") +write(joinpath("_layout", "page_foot.html"), "") +write("config.md", "") +write("index.md", """ + @def var = 5 + """) +write("page2.md", """ + @def var = 7 + """) + +@testset "fill2" begin + empty!(F.ALL_PAGE_VARS) + @test F.pagevar("page2", :var) == 7 + @test F.pagevar("index", :var) == 5 + @test F.pagevar("page2.md", :var) == 7 + @test F.pagevar("index.md", :var) == 5 + @test F.pagevar("page2.html", :var) == 7 + @test F.pagevar("index.html", :var) == 5 + s = """ + {{fill var page2}} + """ + r = fd2html(s; internal=true) + @test isapproxstr(r, "7") + s = """ + @def paths = ["page2", "index"] + {{for p in paths}} + {{fill var p}} + {{end}} + """ + r = fd2html(s; internal=true) + @test isapproxstr(r, "7 5") +end diff --git a/test/templating/for.jl b/test/templating/for.jl new file mode 100644 index 000000000..e5ac27d66 --- /dev/null +++ b/test/templating/for.jl @@ -0,0 +1,42 @@ +@testset "for-basic" begin + s = """ + @def v1 = [1, 2, 3] + ~~~ + {{for v in v1}} + v: {{fill v}} + {{end}} + ~~~ + """ |> fd2html_td + @test isapproxstr(s, """ + v: 1 + v: 2 + v: 3 + """) + + s = """ + @def v1 = [[1,2], [3,4]] + ~~~ + {{for (a,b) in v1}} + a: {{fill a}} b: {{fill b}} + {{end}} + ~~~ + """ |> fd2html_td + @test isapproxstr(s, """ + a: 1 b: 2 + a: 3 b: 4 + """) + + s = """ + @def v_1 = ("a"=>1, "b"=>2, "c"=>3) + ~~~ + {{for (a,b) in v_1}} + a: {{fill a}} b: {{fill b}} + {{end}} + ~~~ + """ |> fd2html_td + @test isapproxstr(s, """ + a: a b: 1 + a: b b: 2 + a: c b: 3 + """) +end diff --git a/test/test_utils.jl b/test/test_utils.jl index d59d8f07d..e7f33cc5c 100644 --- a/test/test_utils.jl +++ b/test/test_utils.jl @@ -64,7 +64,7 @@ function explore_md_steps(mds) F.validate_headers!(tokens) hrules = F.find_hrules!(tokens) F.find_indented_blocks!(tokens, mds) - steps[:tokenization] = (tokens=tokens,) + steps[:tokenization] = (tokens=tokens,) # ocblocks blocks, tokens = F.find_all_ocblocks(tokens, F.MD_OCB) @@ -73,7 +73,6 @@ function explore_md_steps(mds) blocks2, tokens = F.find_all_ocblocks(tokens, vcat(F.MD_OCB2, F.MD_OCB_MATH)) append!(blocks, blocks2) F.deactivate_inner_blocks!(blocks) - F.merge_indented_blocks!(blocks, mds) F.filter_indented_blocks!(blocks) steps[:ocblocks] = (blocks=blocks, tokens=tokens) @@ -95,12 +94,18 @@ function explore_md_steps(mds) dbb = F.find_double_brace_blocks(toks_pre_ocb) + F.process_mddefs(blocks, false) + sp_chars = F.find_special_chars(tokens) steps[:spchars] = (spchars=sp_chars,) fnrefs = filter(τ -> τ.name == :FOOTNOTE_REF, tokens) steps[:fnrefs] = (fnrefs=fnrefs,) + if !F.locvar("indented_code") + filter!(b -> b.name != :CODE_BLOCK_IND, blocks) + end + b2insert = F.merge_blocks(lxcoms, F.deactivate_divs(blocks), sp_chars, fnrefs, dbb, hrules) steps[:b2insert] = (b2insert=b2insert,) diff --git a/test/utils_file/basic.jl b/test/utils_file/basic.jl new file mode 100644 index 000000000..c301b6a5f --- /dev/null +++ b/test/utils_file/basic.jl @@ -0,0 +1,72 @@ +# Tests for what happens in the `utils.jl` + +fs2() +write(joinpath("_layout", "head.html"), "") +write(joinpath("_layout", "foot.html"), "") +write(joinpath("_layout", "page_foot.html"), "") +write("config.md", "") +fdi(s) = fd2html(s; internal=true) + +@testset "utils1" begin + write("utils.jl", """ + x = 5 + """) + F.process_utils() + s = """ + {{fill x}} + """ |> fdi + @test isapproxstr(s, "5") + s = """ + {{x}} + """ |> fdi + @test isapproxstr(s, "5") +end + +@testset "utils:hfun" begin + write("utils.jl", """ + hfun_foo() = return "blah" + """) + F.process_utils() + s = """ + {{foo}} + """ |> fdi + @test isapproxstr(s, "blah") + + write("utils.jl", """ + function hfun_bar(vname) + val = locvar(vname[1]) + return round(sqrt(val), digits=2) + end + """) + F.process_utils() + s = """ + @def xx = 25 + {{bar xx}} + """ |> fdi + @test isapproxstr(s, "5.0") +end + +@testset "utils:lxfun" begin + write("utils.jl", """ + function lx_foo(lxc::Franklin.LxCom, _) + return uppercase(Franklin.content(lxc.braces[1])) + end + """) + F.process_utils() + s = raw""" + \foo{bar} + """ |> fdi + @test isapproxstr(s, "BAR") + + write("utils.jl", """ + function lx_baz(com, _) + brace_content = Franklin.content(com.braces[1]) + return uppercase(brace_content) + end + """) + F.process_utils() + s = raw""" + \baz{bar} + """ |> fdi + @test isapproxstr(s, "BAR") +end