├── .codecov.yml ├── test ├── _dummies │ ├── scripts │ │ ├── output │ │ │ ├── s1a.png │ │ │ └── s1.txt │ │ └── s1.jl │ ├── 182.md │ └── 151.md ├── global │ ├── html_esc.jl │ ├── rss.jl │ ├── ordering.jl │ ├── postprocess.jl │ └── eval.jl ├── eval │ ├── integration.jl │ ├── module.jl │ ├── extras.jl │ ├── run.jl │ ├── io.jl │ ├── codeblock.jl │ └── io_fs2.jl ├── coverage │ ├── paths.jl │ └── extras1.jl ├── parser │ ├── latex++.jl │ ├── footnotes+links.jl │ ├── indentation++.jl │ ├── md-dbb.jl │ ├── 2-blocks.jl │ ├── 1-tokenize.jl │ └── markdown-extra.jl ├── templating │ ├── fill.jl │ └── for.jl ├── manager │ ├── page_vars_html.jl │ ├── config.jl │ ├── config_fs2.jl │ ├── dir_utils.jl │ ├── rss.jl │ ├── utils.jl │ └── utils_fs2.jl ├── integration │ ├── literate_extras.jl │ ├── literate.jl │ └── literate_fs2.jl ├── converter │ ├── md │ │ ├── markdown4.jl │ │ ├── md_defs.jl │ │ ├── hyperref.jl │ │ ├── tags.jl │ │ ├── markdown.jl │ │ └── markdown2.jl │ ├── html │ │ ├── html2.jl │ │ ├── html_for.jl │ │ └── html.jl │ └── lx │ │ └── input.jl ├── utils │ ├── paths_vars.jl │ ├── html.jl │ ├── folder_structure.jl │ └── errors.jl ├── utils_file │ └── basic.jl └── runtests.jl ├── .gitignore ├── .github └── workflows │ ├── TagBot.yml │ └── CompatHelper.yml ├── .travis.yml ├── src ├── eval │ ├── README.md │ ├── module.jl │ ├── literate.jl │ ├── run.jl │ └── codeblock.jl ├── utils │ ├── errors.jl │ └── paths.jl ├── scripts │ └── minify.py ├── manager │ └── extras.jl ├── converter │ ├── latex │ │ ├── commands.jl │ │ ├── latex.jl │ │ └── hyperrefs.jl │ ├── html │ │ ├── link_fixer.jl │ │ ├── html.jl │ │ └── prerender.jl │ └── markdown │ │ ├── mddefs.jl │ │ ├── tags.jl │ │ └── utils.jl ├── parser │ ├── latex │ │ └── tokens.jl │ └── html │ │ ├── blocks.jl │ │ └── tokens.jl ├── build.jl └── regexes.jl ├── appveyor.yml ├── LICENSE.md └── Project.toml /.codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | -------------------------------------------------------------------------------- /test/_dummies/scripts/output/s1a.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/_dummies/scripts/output/s1.txt: -------------------------------------------------------------------------------- 1 | hello 2 | -------------------------------------------------------------------------------- /test/_dummies/scripts/s1.jl: -------------------------------------------------------------------------------- 1 | const A = 2 2 | println("A is $A") 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.jl.cov 2 | *.jl.*.cov 3 | *.jl.mem 4 | *DS_Store 5 | Manifest.toml 6 | sandbox/ 7 | docs/build/ 8 | test/__tmp/ 9 | -------------------------------------------------------------------------------- /.github/workflows/TagBot.yml: -------------------------------------------------------------------------------- 1 | name: TagBot 2 | on: 3 | schedule: 4 | - cron: 0 * * * * 5 | jobs: 6 | TagBot: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: JuliaRegistries/TagBot@v1 10 | with: 11 | token: ${{ secrets.GITHUB_TOKEN }} 12 | -------------------------------------------------------------------------------- /test/global/html_esc.jl: -------------------------------------------------------------------------------- 1 | # See https://github.com/tlienart/Franklin.jl/issues/326 2 | 3 | @testset "Issue 326" begin 4 | h1 = "
Blah
" 5 | h1e = Markdown.htmlesc(h1) 6 | @test F.is_html_escaped(h1e) 7 | @test F.html_unescape(h1e) == h1 8 | end 9 | -------------------------------------------------------------------------------- /test/_dummies/182.md: -------------------------------------------------------------------------------- 1 | @def hascode = true 2 | 3 | Code block: 4 | 5 | ```julia:./test 6 | #hideall 7 | temps = (15, 23, 20, 15, 20, 17, 18, 30, 21, 19, 17) 8 | a = sum(temps)/length(temps) 9 | println("The _average_ temperature is **$(round(a, digits=1))°C**.") 10 | ``` 11 | 12 | \textoutput{./test} 13 | 14 | The end. 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Documentation: http://docs.travis-ci.com/user/languages/julia/ 2 | language: julia 3 | os: 4 | - linux 5 | julia: 6 | - 1.3 7 | - 1.4 8 | - nightly 9 | matrix: 10 | allow_failures: 11 | - julia: nightly 12 | notifications: 13 | email: false 14 | after_success: 15 | # push coverage results to Codecov 16 | - julia -e 'using Pkg; pkg"add Coverage"; using Coverage; Codecov.submit(Codecov.process_folder())' 17 | -------------------------------------------------------------------------------- /src/eval/README.md: -------------------------------------------------------------------------------- 1 | # Readme - Code evaluation 2 | 3 | Parts of the code here has been adapted from [`Weave.jl`](https://github.com/JunoLab/Weave.jl) in particular code pertaining to [running code](https://github.com/JunoLab/Weave.jl/blob/master/src/run.jl). 4 | 5 | Weave is released by Matti Pastel under the MIT expat [license](https://github.com/JunoLab/Weave.jl/blob/master/LICENSE.md). 6 | 7 | Functions that are taken/adapted from Weave indicate this in their docstring. 8 | -------------------------------------------------------------------------------- /test/eval/integration.jl: -------------------------------------------------------------------------------- 1 | # see also https://github.com/tlienart/Franklin.jl/issues/330 2 | @testset "locvar" begin 3 | s = raw""" 4 | @def va = 5 5 | @def vb = 7 6 | ```julia:ex 7 | #hideall 8 | println(locvar("va")+locvar("vb")) 9 | ``` 10 | \output{ex} 11 | """ |> fd2html_td 12 | @test isapproxstr(s, """ 13 |
12
14 | """) 15 | end 16 | -------------------------------------------------------------------------------- /test/coverage/paths.jl: -------------------------------------------------------------------------------- 1 | @testset "filecmp" begin 2 | gotd() 3 | 4 | p1 = joinpath(td, "hello.md") 5 | p2 = joinpath(td, "bye.md") 6 | p3 = joinpath(td, "cp1.md") 7 | 8 | write(p1, "foo") 9 | write(p2, "foo") 10 | 11 | cp(p1, p3) 12 | 13 | @test F.filecmp(p1, p1) 14 | @test F.filecmp(p1, p2) 15 | @test F.filecmp(p1, p3) 16 | 17 | write(p2, "baz") 18 | @test !F.filecmp(p1, p2) 19 | 20 | @test !F.filecmp(p1, "foo/bar") 21 | end 22 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - julia_version: 1.1 4 | - julia_version: 1.2 5 | - julia_version: 1.3 6 | - julia_version: 1.4 7 | - julia_version: nightly 8 | 9 | platform: 10 | - x64 # 64-bit 11 | 12 | branches: 13 | only: 14 | - master 15 | 16 | notifications: 17 | - provider: Email 18 | on_build_success: false 19 | on_build_failure: false 20 | on_build_status_changed: false 21 | 22 | install: 23 | - ps: iex ((new-object net.webclient).DownloadString("https://raw.githubusercontent.com/JuliaCI/Appveyor.jl/version-1/bin/install.ps1")) 24 | 25 | build_script: 26 | - C:\julia\bin\julia -e "%JL_BUILD_SCRIPT%" 27 | 28 | test_script: 29 | - echo "%JL_TEST_SCRIPT%" 30 | - C:\julia\bin\julia -e "%JL_TEST_SCRIPT%" 31 | -------------------------------------------------------------------------------- /test/eval/module.jl: -------------------------------------------------------------------------------- 1 | @testset "utils" begin 2 | # Module name 3 | path = "blah/index.md" 4 | mn = F.modulename("blah/index.md") 5 | @test mn == "FD_SANDBOX_$(hash(path))" 6 | # New module 7 | mod = F.newmodule(mn) 8 | # ismodule 9 | @test F.ismodule(mn) 10 | @test !F.ismodule("foobar") 11 | foobar = 7 12 | @test !F.ismodule("foobar") 13 | # eval in module 14 | Core.eval(mod, Meta.parse("const a=5", 1)[1]) 15 | @test isdefined(mod, :a) 16 | @test isconst(mod, :a) 17 | # overwrite module 18 | mod = F.newmodule(mn) 19 | @test F.ismodule(mn) 20 | @test !isdefined(mod, :a) 21 | Core.eval(mod, Meta.parse("a = 7", 1)[1]) 22 | @test isdefined(mod, :a) 23 | @test !isconst(mod, :a) 24 | end 25 | -------------------------------------------------------------------------------- /.github/workflows/CompatHelper.yml: -------------------------------------------------------------------------------- 1 | name: CompatHelper 2 | 3 | on: 4 | schedule: 5 | - cron: '00 * * * *' 6 | issues: 7 | types: [opened, reopened] 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | julia-version: [1.2.0] 15 | julia-arch: [x86] 16 | os: [ubuntu-latest] 17 | steps: 18 | - uses: julia-actions/setup-julia@latest 19 | with: 20 | version: ${{ matrix.julia-version }} 21 | - name: Pkg.add("CompatHelper") 22 | run: julia -e 'using Pkg; Pkg.add("CompatHelper")' 23 | - name: CompatHelper.main() 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | run: julia -e 'using CompatHelper; CompatHelper.main()' 27 | -------------------------------------------------------------------------------- /test/parser/latex++.jl: -------------------------------------------------------------------------------- 1 | # Let's test latex commands to death... especially in light of #444 2 | 3 | @testset "lx++1" begin 4 | # 444 5 | s = "\\newcommand{\\note}[1]{#1} \\note{A `B` C} D" |> fd2html_td 6 | @test isapproxstr(s, """ 7 | A B C D 8 | """) 9 | s = raw""" 10 | \newcommand{\note}[1]{@@note #1 @@} 11 | \note{A} 12 | \note{A `B` C} 13 | \note{A @@cc B @@ D} 14 | \note{A @@cc B `D` E @@ F} 15 | """ |> fd2html_td 16 | @test isapproxstr(s, """ 17 |
A
18 |
A B C
19 |
A
B
D
20 |
A
B D E
F
21 | """) 22 | end 23 | -------------------------------------------------------------------------------- /test/global/rss.jl: -------------------------------------------------------------------------------- 1 | # simple test that things get created 2 | @testset "RSS gen" begin 3 | f = joinpath(p, "basic", "__site", "feed.xml") 4 | @test isfile(f) 5 | fc = prod(readlines(f, keep=true)) 6 | 7 | @test occursin(raw"""""", fc) 8 | @test occursin(raw"""Franklin Template""", fc) 9 | @test occursin(raw"""""", fc) 11 | @test !occursin(raw"""""", fc) 12 | @test occursin(raw"""https://tlienart.github.io/FranklinTemplates.jl/menu1/index.html""", fc) 13 | @test occursin(raw"""blurb in a RSS feed;""", fc) 14 | end 15 | -------------------------------------------------------------------------------- /test/templating/fill.jl: -------------------------------------------------------------------------------- 1 | fs2() 2 | write(joinpath("_layout", "head.html"), "") 3 | write(joinpath("_layout", "foot.html"), "") 4 | write(joinpath("_layout", "page_foot.html"), "") 5 | write("config.md", "") 6 | 7 | @testset "fill2" begin 8 | write("index.md", """ 9 | @def var = 5 10 | {{fill var page2}} 11 | """) 12 | write("page2.md", """ 13 | @def var = 7 14 | {{fill var index}} 15 | """) 16 | F.serve(single=true, clear=true, cleanup=false) 17 | index = joinpath("__site", "index.html") 18 | pg2 = joinpath("__site", "page2", "index.html") 19 | @test isapproxstr(read(index, String), """ 20 |
7
21 | """) 22 | @test isapproxstr(read(pg2, String), """ 23 |
5
24 | """) 25 | end 26 | -------------------------------------------------------------------------------- /test/manager/page_vars_html.jl: -------------------------------------------------------------------------------- 1 | fs2() 2 | 3 | @testset "Scope (#412)" begin 4 | write(joinpath(td, "config.md"), """ 5 | @def title = "config" 6 | """) 7 | write(joinpath(td, "page.md"), """ 8 | @def title = "page" 9 | """) 10 | write(joinpath(td, "index.html"), """ 11 | {{insert head.html}} 12 | """) 13 | mkpath(joinpath(td, "_layout")) 14 | mkpath(joinpath(td, "_css")) 15 | write(joinpath(td, "_layout", "head.html"), "

{{fill title}}

") 16 | write(joinpath(td, "_layout", "page_foot.html"), "") 17 | write(joinpath(td, "_layout", "foot.html"), "") 18 | serve(single=true) 19 | 20 | @test startswith(read(joinpath("__site", "index.html"), String), 21 | "

config

") 22 | @test startswith(read(joinpath("__site", "page", "index.html"), String), 23 | "

page

") 24 | end 25 | -------------------------------------------------------------------------------- /test/integration/literate_extras.jl: -------------------------------------------------------------------------------- 1 | fs2() 2 | 3 | @testset "(no)show" begin 4 | lit = raw""" 5 | # A 6 | 7 | 1 + 1 8 | 9 | # B 10 | 11 | 2^2; 12 | 13 | # C 14 | 15 | println("hello") 16 | 17 | # done. 18 | """ 19 | lp = mkpath(joinpath(F.PATHS[:folder], "_literate")) 20 | write(joinpath(lp, "ex1.jl"), lit) 21 | 22 | s = raw""" 23 | @def showall = true 24 | INI 25 | \literate{/_literate/ex1} 26 | """ 27 | 28 | h = s |> fd2html_td 29 | @test isapproxstr(h, """ 30 |

INI A

31 |
1 + 1
2
32 |

B

33 |
2^2;
34 |

C

35 |
println("hello")
hello
36 |         
37 |

done.

38 | """) 39 | end 40 | -------------------------------------------------------------------------------- /test/templating/for.jl: -------------------------------------------------------------------------------- 1 | @testset "for-basic" begin 2 | s = """ 3 | @def v1 = [1, 2, 3] 4 | ~~~ 5 | {{for v in v1}} 6 | v: {{fill v}} 7 | {{end}} 8 | ~~~ 9 | """ |> fd2html_td 10 | @test isapproxstr(s, """ 11 | v: 1 12 | v: 2 13 | v: 3 14 | """) 15 | 16 | s = """ 17 | @def v1 = [[1,2], [3,4]] 18 | ~~~ 19 | {{for (a,b) in v1}} 20 | a: {{fill a}} b: {{fill b}} 21 | {{end}} 22 | ~~~ 23 | """ |> fd2html_td 24 | @test isapproxstr(s, """ 25 | a: 1 b: 2 26 | a: 3 b: 4 27 | """) 28 | 29 | s = """ 30 | @def v_1 = ("a"=>1, "b"=>2, "c"=>3) 31 | ~~~ 32 | {{for (a,b) in v_1}} 33 | a: {{fill a}} b: {{fill b}} 34 | {{end}} 35 | ~~~ 36 | """ |> fd2html_td 37 | @test isapproxstr(s, """ 38 | a: a b: 1 39 | a: b b: 2 40 | a: c b: 3 41 | """) 42 | end 43 | -------------------------------------------------------------------------------- /test/_dummies/151.md: -------------------------------------------------------------------------------- 1 | @def title = "Julia" 2 | @def hascode = true 3 | 4 | 5 | ```julia 6 | add OhMyREPL#master 7 | ``` 8 | 9 | AAA 10 | 11 | ~~~ 12 |
"""
13 |     bar(x[, y])
14 | 
15 | BBB
16 | 
17 | # Examples
18 | ```jldoctest
19 | D
20 | ```
21 | """
22 | function bar(x, y)
23 |     ...
24 | end
25 | 
26 | ~~~ 27 | 28 | For complex functions with multiple arguments use a argument list, also 29 | if there are many keyword arguments use ``: 30 | 31 | ~~~ 32 |
"""
33 |     matdiag(diag, nr, nc; <keyword arguments>)
34 | 
35 | Create Matrix with number `vdiag` on the super- or subdiagonals and `vndiag`
36 | in the rest.
37 | 
38 | # Arguments
39 | - `diag::Number`: `Number` to write into created super- or subdiagonal
40 | 
41 | # Examples
42 | ```jldoctest
43 | julia> matdiag(true, 5, 5, sr=2, ec=3)
44 | ```
45 | """
46 | function
47 | matdiag(diag::Number, nr::Integer, nc::Integer;)
48 |     ...
49 | end
50 | 
51 | ~~~ 52 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The Franklin.jl package is licensed under the MIT "Expat" License: 2 | 3 | > Copyright (c) 2018-2020: Thibaut Lienart. 4 | > 5 | > Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in all 13 | > copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | > SOFTWARE. 22 | > 23 | -------------------------------------------------------------------------------- /Project.toml: -------------------------------------------------------------------------------- 1 | name = "Franklin" 2 | uuid = "713c75ef-9fc9-4b05-94a9-213340da978e" 3 | authors = ["Thibaut Lienart "] 4 | version = "0.8.1" 5 | 6 | [deps] 7 | Crayons = "a8cc5b0e-0ffa-5ad4-8c14-923d3ee1735f" 8 | Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" 9 | DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" 10 | DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" 11 | FranklinTemplates = "3a985190-f512-4703-8d38-2a7944ed5916" 12 | HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3" 13 | Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" 14 | LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589" 15 | Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" 16 | Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" 17 | NodeJS = "2bd173c7-0d6d-553b-b6af-13a54713934c" 18 | OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" 19 | Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" 20 | Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" 21 | 22 | [compat] 23 | Crayons = "4" 24 | DocStringExtensions = "0.8" 25 | FranklinTemplates = "0.6,0.7" 26 | HTTP = "0.8" 27 | Literate = "2.2" 28 | LiveServer = "0.3" 29 | NodeJS = "0.6,1" 30 | OrderedCollections = "1.1" 31 | julia = "1.1" 32 | 33 | [extras] 34 | DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" 35 | LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" 36 | Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" 37 | Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" 38 | 39 | [targets] 40 | test = ["Test", "DataStructures", "Random", "LinearAlgebra"] 41 | -------------------------------------------------------------------------------- /test/manager/config.jl: -------------------------------------------------------------------------------- 1 | # additional tests for config 2 | fs1() 3 | 4 | foofig(p, s) = (write(joinpath(p, "src", "config.md"), s); F.process_config()) 5 | 6 | @testset "config" begin 7 | p = joinpath(D, "..", "__tst_config") 8 | isdir(p) && rm(p; recursive=true, force=true) 9 | mkdir(p); cd(p); 10 | F.FOLDER_PATH[] = pwd() 11 | F.set_paths!() 12 | mkdir(joinpath(p, "src")) 13 | # ================================ 14 | # asssignments go to GLOBAL 15 | foofig(p, raw""" 16 | @def var = 5 17 | """) 18 | @test haskey(F.GLOBAL_VARS, "var") 19 | @test F.GLOBAL_VARS["var"][1] == 5 20 | 21 | # lxdefs go to GLOBAL 22 | foofig(p, raw""" 23 | \newcommand{\hello}{goodbye} 24 | """) 25 | @test haskey(F.GLOBAL_LXDEFS, "\\hello") 26 | @test F.GLOBAL_LXDEFS["\\hello"].def == "goodbye" 27 | 28 | # combination of lxdefs 29 | foofig(p, raw""" 30 | \newcommand{\hello}{goodbye} 31 | \newcommand{\hellob}{\hello} 32 | \newcommand{\helloc}{\hellob} 33 | """) 34 | 35 | @test F.GLOBAL_LXDEFS["\\hello"].from < F.GLOBAL_LXDEFS["\\hello"].to < 36 | F.GLOBAL_LXDEFS["\\hellob"].from < F.GLOBAL_LXDEFS["\\hellob"].to < 37 | F.GLOBAL_LXDEFS["\\helloc"].from < F.GLOBAL_LXDEFS["\\helloc"].to 38 | 39 | @test fd2html(raw"""\helloc"""; dir=p, internal=true) == "goodbye" 40 | 41 | # ================================ 42 | # go back and cleanup 43 | cd(R); rm(p; recursive=true, force=true) 44 | end 45 | -------------------------------------------------------------------------------- /test/manager/config_fs2.jl: -------------------------------------------------------------------------------- 1 | # additional tests for config 2 | fs2() 3 | 4 | foofig(s) = (write(joinpath(td, "config.md"), s); F.process_config()) 5 | 6 | @testset "config" begin 7 | # ================================ 8 | # asssignments go to GLOBAL 9 | foofig(raw""" 10 | @def var = 5 11 | """) 12 | @test haskey(F.GLOBAL_VARS, "var") 13 | @test F.GLOBAL_VARS["var"][1] == 5 14 | 15 | # lxdefs go to GLOBAL 16 | foofig(raw""" 17 | \newcommand{\hello}{goodbye} 18 | """) 19 | @test haskey(F.GLOBAL_LXDEFS, "\\hello") 20 | @test F.GLOBAL_LXDEFS["\\hello"].def == "goodbye" 21 | 22 | # combination of lxdefs 23 | foofig(raw""" 24 | \newcommand{\hello}{goodbye} 25 | \newcommand{\hellob}{\hello} 26 | \newcommand{\helloc}{\hellob} 27 | """) 28 | 29 | @test F.GLOBAL_LXDEFS["\\hello"].from < F.GLOBAL_LXDEFS["\\hello"].to < 30 | F.GLOBAL_LXDEFS["\\hellob"].from < F.GLOBAL_LXDEFS["\\hellob"].to < 31 | F.GLOBAL_LXDEFS["\\helloc"].from < F.GLOBAL_LXDEFS["\\helloc"].to 32 | 33 | @test fd2html(raw"""\helloc"""; dir=td, internal=true) == "goodbye" 34 | end 35 | 36 | @testset "i381" begin 37 | foofig(raw""" 38 | @def tags = [] 39 | """) 40 | 41 | s = """ 42 | @def tags = ["tag1", "tag2"] 43 | 44 | ~~~ 45 | {{for tag in tags}} 46 | {{fill tag}} 47 | {{end}} 48 | ~~~ 49 | """ |> fd2html_td 50 | @test isapproxstr(s, "tag1 tag2") 51 | end 52 | -------------------------------------------------------------------------------- /test/eval/extras.jl: -------------------------------------------------------------------------------- 1 | fs2() 2 | 3 | @testset "fig/rpath" begin 4 | gotd() 5 | 6 | mkpath(joinpath(td, "__site", "assets", "index")) 7 | write(joinpath(td, "__site", "assets", "index", "baz.png"), "baz") 8 | 9 | mkpath(joinpath(td, "__site", "assets", "blog", "kaggle")) 10 | write(joinpath(td, "__site", "assets", "blog", "kaggle", "foo.png"), "foo") 11 | 12 | s = raw""" 13 | @def fd_rpath = "index.md" 14 | ABC 15 | \fig{./baz.png} 16 | """ |> fd2html_td 17 | @test isapproxstr(s, """ 18 |

ABC

19 | """) 20 | 21 | s = raw""" 22 | @def fd_rpath = "index.md" 23 | ABC 24 | \fig{./unknown.png} 25 | """ |> fd2html_td 26 | @test isapproxstr(s, """ 27 |

ABC

// Image matching '/assets/index/unknown.png' not found. //

28 | """) 29 | 30 | s = raw""" 31 | @def fd_rpath = "blog/kaggle/index.md" 32 | ABC 33 | \fig{./foo.png} 34 | """ |> fd2html_td 35 | @test isapproxstr(s, """ 36 |

ABC

37 | """) 38 | 39 | s = raw""" 40 | @def fd_rpath = "blog/kaggle/index.md" 41 | ABC 42 | \fig{./baz.png} 43 | """ |> fd2html_td 44 | @test isapproxstr(s, """ 45 |

ABC

// Image matching '/assets/blog/kaggle/baz.png' not found. //

46 | """) 47 | end 48 | -------------------------------------------------------------------------------- /src/eval/module.jl: -------------------------------------------------------------------------------- 1 | #= 2 | Functionalities to generate a sandbox module. 3 | =# 4 | 5 | """ 6 | $SIGNATURES 7 | 8 | Return a sandbox module name corresponding to the page at `fpath`, effectively 9 | `FD_SANDBOX_*` where `*` is a hash of the path. 10 | """ 11 | modulename(fpath::AS) = "FD_SANDBOX_$(hash(fpath))" 12 | 13 | """ 14 | $SIGNATURES 15 | 16 | Checks whether a name is a defined module. 17 | """ 18 | function ismodule(name::String)::Bool 19 | s = Symbol(name) 20 | isdefined(Main, s) || return false 21 | typeof(getfield(Main, s)) === Module 22 | end 23 | 24 | """ 25 | $SIGNATURES 26 | 27 | Creates a new module with a given name, if the module exists, it is wiped. 28 | Discards the warning message that a module is replaced which may otherwise 29 | happen. Return a handle pointing to the module. 30 | """ 31 | function newmodule(name::String)::Module 32 | mod = nothing 33 | junk = tempname() 34 | open(junk, "w") do outf 35 | # discard the "WARNING: redefining module X" 36 | redirect_stderr(outf) do 37 | mod = Core.eval(Main, Meta.parse(""" 38 | module $name 39 | import Franklin 40 | import Franklin: @OUTPUT, fdplotly, locvar, 41 | pagevar, globvar, fd2html, get_url 42 | if isdefined(Main, :Utils) && typeof(Main.Utils) == Module 43 | import ..Utils 44 | end 45 | end 46 | """)) 47 | end 48 | end 49 | return mod 50 | end 51 | -------------------------------------------------------------------------------- /src/utils/errors.jl: -------------------------------------------------------------------------------- 1 | abstract type FranklinException <: Exception end 2 | 3 | # 4 | # Parsing related 5 | # 6 | 7 | """An OCBlock was not parsed properly (e.g. the closing token was not found).""" 8 | struct OCBlockError <: FranklinException 9 | m::String 10 | c::String 11 | end 12 | 13 | function Base.showerror(io::IO, be::OCBlockError) 14 | println(io, be.m) 15 | print(io, be.c) 16 | end 17 | 18 | """A `\\newcommand` was not parsed properly.""" 19 | struct LxDefError <: FranklinException 20 | m::String 21 | end 22 | 23 | """A latex command was found but could not be processed properly.""" 24 | struct LxComError <: FranklinException 25 | m::String 26 | end 27 | 28 | """A math block name failed to parse.""" 29 | struct MathBlockError <: FranklinException 30 | m::String 31 | end 32 | 33 | """A Page Variable wasn't set properly.""" 34 | struct PageVariableError <: FranklinException 35 | m::String 36 | end 37 | 38 | # 39 | # HTML related 40 | # 41 | 42 | """An HTML block (e.g. [`HCond`](@see)) was erroneous.""" 43 | struct HTMLBlockError <: FranklinException 44 | m::String 45 | end 46 | 47 | """An HTML function (e.g. `{{fill ...}}`) failed.""" 48 | struct HTMLFunctionError <: FranklinException 49 | m::String 50 | end 51 | 52 | # 53 | # ASSET PATH error 54 | # 55 | 56 | """A relative path was erroneous.""" 57 | struct RelativePathError <: FranklinException 58 | m::String 59 | end 60 | 61 | """A file was not found.""" 62 | struct FileNotFoundError <: FranklinException 63 | m::String 64 | end 65 | 66 | # 67 | # CODE 68 | # 69 | 70 | """A relative path was erroneous for Literate.""" 71 | struct LiterateRelativePathError <: FranklinException 72 | m::String 73 | end 74 | -------------------------------------------------------------------------------- /test/parser/footnotes+links.jl: -------------------------------------------------------------------------------- 1 | @testset "footnotes" begin 2 | set_curpath("index.md") 3 | st = """ 4 | A[^1] B[^blah] C 5 | """ 6 | @test isapproxstr(st |> seval, """ 7 |

A[1] 8 | B[2] 9 | C

""") 10 | 11 | st = """ 12 | A[^1] B[^blah] 13 | C 14 | [^1]: first footnote 15 | [^blah]: second footnote 16 | """ 17 | @test isapproxstr(st |> seval, """ 18 |

19 | A 20 | [1] 21 | B[2] 22 | C 23 | 24 | 25 | 26 | 27 | 28 |
[1]first footnote
29 | 30 | 31 | 32 | 33 | 34 |
[2]second footnote
35 |

""") 36 | end 37 | 38 | 39 | @testset "Fn in code" begin 40 | s = raw""" 41 | ```markdown 42 | this has[^1] 43 | 44 | [^1]: def 45 | ``` 46 | blah 47 | """ 48 | @test isapproxstr(s |> fd2html_td, """ 49 |
this has[^1]
50 |         [^1]: def
51 |         
blah""") 52 | end 53 | -------------------------------------------------------------------------------- /test/parser/indentation++.jl: -------------------------------------------------------------------------------- 1 | @testset "indentation" begin 2 | mds = """ 3 | A 4 | B 5 | C 6 | D""" 7 | 8 | tokens = F.find_tokens(mds, F.MD_TOKENS, F.MD_1C_TOKENS) 9 | F.find_indented_blocks!(tokens, mds) 10 | toks = deepcopy(tokens) 11 | 12 | @test tokens[1].name == :LR_INDENT 13 | @test tokens[2].name == :LR_INDENT 14 | @test tokens[3].name == :LINE_RETURN 15 | 16 | ocp = F.OCProto(:CODE_BLOCK_IND, :LR_INDENT, (:LINE_RETURN,), false) 17 | 18 | blocks, tokens = F.find_ocblocks(tokens, ocp) 19 | 20 | @test blocks[1].name == :CODE_BLOCK_IND 21 | @test F.content(blocks[1]) == "B\n C" 22 | 23 | blocks, tokens = F.find_all_ocblocks(toks, [ocp]) 24 | 25 | @test blocks[1].name == :CODE_BLOCK_IND 26 | @test F.content(blocks[1]) == "B\n C" 27 | 28 | mds = """ 29 | @def indented_code = true 30 | A 31 | 32 | B 33 | C 34 | D""" 35 | steps = explore_md_steps(mds) 36 | toks = steps[:tokenization].tokens 37 | @test toks[4].name == toks[5].name == :LR_INDENT 38 | blk = steps[:ocblocks].blocks 39 | @test blk[2].name == :CODE_BLOCK_IND 40 | b2i = steps[:b2insert].b2insert 41 | @test b2i[2].name == :CODE_BLOCK_IND 42 | @test isapproxstr(mds |> fd2html_td, """ 43 |

A

B
44 |         C
D

45 | """) 46 | end 47 | 48 | @testset "ind+lx" begin 49 | s = raw""" 50 | \newcommand{\julia}[1]{ 51 | ```julia 52 | #1 53 | ``` 54 | } 55 | Hello 56 | \julia{a=5 57 | x=3} 58 | """ |> fd2html_td 59 | @test isapproxstr(s, """ 60 |

Hello

a=5
61 |         x=3

62 | """) 63 | end 64 | -------------------------------------------------------------------------------- /src/scripts/minify.py: -------------------------------------------------------------------------------- 1 | # This is a simple script using `css_html_js_minify` (available via pip) to compress html and css 2 | # files (the js that we use is already compressed). This script takes negligible time to run. 3 | 4 | import os 5 | from css_html_js_minify import process_single_html_file as min_html 6 | from css_html_js_minify import process_single_css_file as min_css 7 | from multiprocessing import Pool, cpu_count 8 | from functools import partial 9 | 10 | # modify those if you're not using the standard output paths. 11 | if old_folder_structure: 12 | CSS, PUB = "css", "pub" 13 | html_files = ["index.html"] 14 | for root, dirs, files in os.walk(PUB): 15 | for fname in files: 16 | if fname.endswith(".html"): 17 | html_files.append(os.path.join(root, fname)) 18 | 19 | css_files = [] 20 | 21 | for root, dirs, files in os.walk(CSS): 22 | for fname in files: 23 | if fname.endswith(".css"): 24 | css_files.append(os.path.join(root, fname)) 25 | else: 26 | html_files = [] 27 | css_files = [] 28 | for root, dirs, files in os.walk("__site"): 29 | for fname in files: 30 | if fname.endswith(".html"): 31 | html_files.append(os.path.join(root, fname)) 32 | if fname.endswith(".css"): 33 | css_files.append(os.path.join(root, fname)) 34 | 35 | if os.name == 'nt': 36 | # multiprocessing doesn't seem to go well with windows... 37 | for file in html_files: 38 | min_html(file, overwrite=True) 39 | for file in css_files: 40 | min_css(file, overwrite=True) 41 | else: 42 | pool = Pool(cpu_count()) 43 | 44 | pool.map_async(partial(min_html, overwrite=True), html_files) 45 | pool.map_async(partial(min_css, overwrite=True), css_files) 46 | 47 | pool.close() 48 | pool.join() 49 | -------------------------------------------------------------------------------- /test/converter/md/markdown4.jl: -------------------------------------------------------------------------------- 1 | @testset "latex-wspace" begin 2 | s = raw""" 3 | \newcommand{\hello}{hello} 4 | A\hello B 5 | """ |> fd2html_td 6 | @test isapproxstr(s, "

Ahello B

") 7 | s = raw""" 8 | \newcommand{\eqa}[1]{\begin{eqnarray}#1\end{eqnarray}} 9 | A\eqa{B}C 10 | \eqa{ 11 | D 12 | }E 13 | """ |> fd2html_td 14 | @test isapproxstr(s, raw""" 15 |

16 | A\[\begin{array}{c} B\end{array}\]C 17 | \[\begin{array}{c} D\end{array}\]E 18 |

""") 19 | 20 | s = raw""" 21 | \newcommand{\eqa}[1]{\begin{eqnarray}#1\end{eqnarray}} 22 | \eqa{A\\ 23 | D 24 | }E 25 | """ |> fd2html_td 26 | @test isapproxstr(s, raw""" 27 | \[\begin{array}{c} A\\ 28 | D\end{array}\]E 29 | """) 30 | s = raw""" 31 | @def indented_code = false 32 | \newcommand{\eqa}[1]{\begin{eqnarray}#1\end{eqnarray}} 33 | \eqa{A\\ 34 | D}E""" |> fd2html_td 35 | @test isapproxstr(s, raw""" 36 | \[\begin{array}{c} A\\ 37 | D\end{array}\]E 38 | """) 39 | end 40 | 41 | @testset "latex-wspmath" begin 42 | s = raw""" 43 | \newcommand{\esp}{\quad\!\!} 44 | $$A\esp=B$$ 45 | """ |> fd2html_td 46 | @test isapproxstr(s, raw"\[A\quad\!\!=B\]") 47 | end 48 | 49 | @testset "code-wspace" begin 50 | s = raw""" 51 | A 52 | ``` 53 | C 54 | B 55 | E 56 | D 57 | ``` 58 | """ |> fd2html_td 59 | @test isapproxstr(s, """

A

C\n    B\n        E\nD

\n""") 60 | end 61 | 62 | @testset "auto html esc" begin 63 | s = raw""" 64 | Blah 65 | ```html 66 |
Blah
67 | ``` 68 | End 69 | """ |> fd2html_td 70 | @test isapproxstr(s, """ 71 |

72 | Blah

<div class="foo">Blah</div>
73 | End

""") 74 | end 75 | -------------------------------------------------------------------------------- /test/manager/dir_utils.jl: -------------------------------------------------------------------------------- 1 | fs2() 2 | 3 | @testset "ignore/fs2" begin 4 | gotd() 5 | s = """ 6 | @def ignore = ["foo.md", "path/foo.md", "dir/", "path/dir/"] 7 | """ 8 | write(joinpath(td, "config.md"), s); 9 | F.process_config() 10 | @test F.globvar("ignore") == ["foo.md", "path/foo.md", "dir/", "path/dir/"] 11 | 12 | write(joinpath(td, "foo.md"), "anything") 13 | mkpath(joinpath(td, "path")) 14 | write(joinpath(td, "path", "foo.md"), "anything") 15 | mkpath(joinpath(td, "dir")) 16 | write(joinpath(td, "dir", "foo1.md"), "anything") 17 | mkpath(joinpath(td, "path", "dir")) 18 | write(joinpath(td, "path", "dir", "foo2.md"), "anything") 19 | write(joinpath(td, "index.md"), "standard things") 20 | watched = F.fd_setup() 21 | @test length(watched.md) == 1 22 | @test first(watched.md).first.second == "index.md" 23 | end 24 | 25 | fs1() 26 | 27 | @testset "ignore/fs1" begin 28 | gotd() 29 | s = """ 30 | @def ignore = ["foo.md", "path/foo.md", "dir/", "path/dir/"] 31 | """ 32 | mkpath(joinpath(td, "src")) 33 | write(joinpath(td, "src", "config.md"), s); 34 | F.process_config() 35 | @test F.globvar("ignore") == ["foo.md", "path/foo.md", "dir/", "path/dir/"] 36 | 37 | write(joinpath(td, "src", "foo.md"), "anything") 38 | mkpath(joinpath(td, "src", "path")) 39 | write(joinpath(td, "src", "path", "foo.md"), "anything") 40 | mkpath(joinpath(td, "src", "dir")) 41 | write(joinpath(td, "src", "dir", "foo1.md"), "anything") 42 | mkpath(joinpath(td, "src", "path", "dir")) 43 | write(joinpath(td, "src", "path", "dir", "foo2.md"), "anything") 44 | write(joinpath(td, "src", "index.md"), "standard things") 45 | 46 | mkpath(joinpath(td, "src", "pages")) 47 | mkpath(joinpath(td, "src", "_css")) 48 | mkpath(joinpath(td, "src", "_html_parts")) 49 | 50 | watched = F.fd_setup() 51 | @test length(watched.md) == 1 52 | @test first(watched.md).first.second == "index.md" 53 | end 54 | -------------------------------------------------------------------------------- /test/converter/html/html2.jl: -------------------------------------------------------------------------------- 1 | @testset "Non-nested" begin 2 | F.set_vars!(F.LOCAL_VARS, [ 3 | "a" => "false", 4 | "b" => "false"]) 5 | 6 | hs = """A{{if a}}B{{elseif b}}C{{else}}D{{end}}E""" 7 | 8 | tokens = F.find_tokens(hs, F.HTML_TOKENS, F.HTML_1C_TOKENS) 9 | hblocks, tokens = F.find_all_ocblocks(tokens, F.HTML_OCB) 10 | qblocks = F.qualify_html_hblocks(hblocks) 11 | 12 | F.set_vars!(F.LOCAL_VARS, ["a"=>"true","b"=>"false"]) 13 | fhs = F.process_html_qblocks(hs, qblocks) 14 | @test isapproxstr(fhs, "ABE") 15 | 16 | F.set_vars!(F.LOCAL_VARS, ["a"=>"false","b"=>"true"]) 17 | fhs = F.process_html_qblocks(hs, qblocks) 18 | @test isapproxstr(fhs, "ACE") 19 | 20 | F.set_vars!(F.LOCAL_VARS, ["a"=>"false","b"=>"false"]) 21 | fhs = F.process_html_qblocks(hs, qblocks) 22 | @test isapproxstr(fhs, "ADE") 23 | end 24 | 25 | @testset "Nested" begin 26 | F.def_LOCAL_VARS!() 27 | F.set_vars!(F.LOCAL_VARS, [ 28 | "a" => "false", 29 | "b" => "false", 30 | "c" => "false"]) 31 | 32 | hs = """A {{if a}} B {{elseif b}} C {{if c}} D {{end}} {{else}} E {{end}} F""" 33 | 34 | tokens = F.find_tokens(hs, F.HTML_TOKENS, F.HTML_1C_TOKENS) 35 | hblocks, tokens = F.find_all_ocblocks(tokens, F.HTML_OCB) 36 | qblocks = F.qualify_html_hblocks(hblocks) 37 | 38 | F.set_vars!(F.LOCAL_VARS, ["a"=>"true"]) 39 | fhs = F.process_html_qblocks(hs, qblocks) 40 | @test isapproxstr(fhs, "ABF") 41 | 42 | F.set_vars!(F.LOCAL_VARS, ["a"=>"false", "b"=>"true", "c"=>"false"]) 43 | fhs = F.process_html_qblocks(hs, qblocks) 44 | @test isapproxstr(fhs, "ACF") 45 | 46 | F.set_vars!(F.LOCAL_VARS, ["a"=>"false", "b"=>"true", "c"=>"true"]) 47 | fhs = F.process_html_qblocks(hs, qblocks) 48 | @test isapproxstr(fhs, "ACDF") 49 | 50 | F.set_vars!(F.LOCAL_VARS, ["a"=>"false", "b"=>"false"]) 51 | fhs = F.process_html_qblocks(hs, qblocks) 52 | @test isapproxstr(fhs, "AEF") 53 | end 54 | -------------------------------------------------------------------------------- /src/manager/extras.jl: -------------------------------------------------------------------------------- 1 | function lunr()::Nothing 2 | prepath = "" 3 | haskey(GLOBAL_VARS, "prepath") && (prepath = GLOBAL_VARS["prepath"].first) 4 | isempty(PATHS) && (FOLDER_PATH[] = pwd(); set_paths!()) 5 | bkdir = pwd() 6 | lunr = joinpath(PATHS[:libs], "lunr") 7 | # is there a lunr folder in /libs/ 8 | isdir(lunr) || 9 | (@warn "No `lunr` folder found in the `/libs/` folder."; return) 10 | # are there the relevant files in /libs/lunr/ 11 | buildindex = joinpath(lunr, "build_index.js") 12 | if !isfile(buildindex) 13 | @warn "No `build_index.js` file found in the `/libs/lunr/` folder." 14 | return nothing 15 | end 16 | # overwrite PATH_PREPEND = ".."; 17 | if !isempty(prepath) 18 | f = String(read(buildindex)) 19 | f = replace(f, 20 | r"const\s+PATH_PREPEND\s*?=\s*?\".*?\"\;" => 21 | "const PATH_PREPEND = \"$(prepath)\";"; count=1) 22 | buildindex = replace(buildindex, r".js$" => ".tmp.js") 23 | write(buildindex, f) 24 | end 25 | cd(lunr) 26 | try 27 | start = time() 28 | msg = rpad("→ Building the Lunr index...", 35) 29 | print(msg) 30 | run(`$NODE $(splitdir(buildindex)[2])`) 31 | print_final(msg, start) 32 | catch e 33 | @warn "There was an error building the Lunr index." 34 | finally 35 | isempty(prepath) || rm(buildindex) 36 | cd(bkdir) 37 | end 38 | return 39 | end 40 | 41 | """ 42 | Display a Plotly plot given an exported JSON `String`. 43 | 44 | ``` 45 | using PlotlyJS 46 | fdplotly(json(plot([1, 2])) 47 | ``` 48 | """ 49 | function fdplotly(json::String; id="fdp"*Random.randstring('a':'z', 3), 50 | style="width:600px;height:350px")::Nothing 51 | println(""" 52 | ~~~ 53 |
54 | 55 | 60 | ~~~ 61 | """) 62 | return nothing 63 | end 64 | -------------------------------------------------------------------------------- /src/converter/latex/commands.jl: -------------------------------------------------------------------------------- 1 | """Convenience function to create pairs (commandname => simple lxdef)""" 2 | lxd(n::String, k::Int, d::String="") = "\\" * n => LxDef("\\" * n, k, subs(d)) 3 | 4 | const LX_INTERNAL_COMMANDS = [ 5 | # --------------- 6 | # Hyperreferences (see converter/latex/hyperrefs.jl) 7 | lxd("eqref", 1), # \eqref{id} 8 | lxd("cite", 1), # \cite{id} 9 | lxd("citet", 1), # \citet{id} 10 | lxd("citep", 1), # \citet{id} 11 | lxd("label", 1), # \label{id} 12 | lxd("biblabel", 2), # \biblabel{id}{name} 13 | lxd("toc", 0), # \toc 14 | # ------------------- 15 | # inclusion / outputs (see converter/latex/io.jl) 16 | lxd("input", 2), # \input{what}{rpath} 17 | lxd("output", 1), # \output{rpath} 18 | lxd("show", 1), # \show{rpath} 19 | lxd("textoutput", 1), # \textoutput{rpath} 20 | lxd("textinput", 1), # \textinput{rpath} 21 | lxd("figalt", 2), # \figalt{alt}{rpath} 22 | lxd("tableinput", 2), # \tableinput{header}{rpath} 23 | lxd("literate", 1), # \literate{rpath} 24 | # ------------------ 25 | # DERIVED / EXPLICIT 26 | lxd("fig", 1, "\\figalt{}{#1}"), 27 | lxd("style", 2, "~~~~~~!#2~~~~~~"), 28 | lxd("underline", 1, "\\style{text-decoration:underline}{!#1}"), 29 | lxd("tableofcontents", 0, "\\toc"), 30 | lxd("codeoutput", 1, "\\output{#1}"), # \codeoutput{rpath} 31 | ] 32 | 33 | """ 34 | List of latex definitions accessible to all pages. This is filled when the 35 | config file is read (via `manager/file_utils.jl:process_config`). 36 | """ 37 | const GLOBAL_LXDEFS = LittleDict{String,LxDef}() 38 | 39 | """ 40 | Convenience function to allocate default values for global latex commands 41 | accessible throughout the site. See [`resolve_lxcom`](@ref). 42 | """ 43 | function def_GLOBAL_LXDEFS!()::Nothing 44 | empty!(GLOBAL_LXDEFS) 45 | for (name, def) in LX_INTERNAL_COMMANDS 46 | GLOBAL_LXDEFS[name] = def 47 | end 48 | nothing 49 | end 50 | -------------------------------------------------------------------------------- /test/utils/paths_vars.jl: -------------------------------------------------------------------------------- 1 | const td = mktempdir() 2 | flush_td() = (isdir(td) && rm(td; recursive=true); mkdir(td)) 3 | F.FOLDER_PATH[] = td 4 | 5 | fd2html_td(e) = fd2html(e; dir=td) 6 | fd2html_tdv(e) = F.fd2html_v(e; dir=td) 7 | 8 | F.def_GLOBAL_VARS!() 9 | F.def_GLOBAL_LXDEFS!() 10 | 11 | @testset "Paths" begin 12 | P = F.set_paths!() 13 | 14 | @test F.PATHS[:folder] == td 15 | @test F.PATHS[:src] == joinpath(td, "src") 16 | @test F.PATHS[:src_css] == joinpath(td, "src", "_css") 17 | @test F.PATHS[:src_html] == joinpath(td, "src", "_html_parts") 18 | @test F.PATHS[:libs] == joinpath(td, "libs") 19 | @test F.PATHS[:pub] == joinpath(td, "pub") 20 | @test F.PATHS[:css] == joinpath(td, "css") 21 | 22 | @test P == F.PATHS 23 | 24 | mkdir(F.PATHS[:src]) 25 | mkdir(F.PATHS[:src_pages]) 26 | mkdir(F.PATHS[:libs]) 27 | mkdir(F.PATHS[:src_css]) 28 | mkdir(F.PATHS[:src_html]) 29 | mkdir(F.PATHS[:assets]) 30 | end 31 | 32 | # copying _libs/katex in the F.PATHS[:libs] so that it can be used in testing 33 | # the js_prerender_math 34 | cp(joinpath(dirname(dirname(pathof(Franklin))), "test", "_libs", "katex"), joinpath(F.PATHS[:libs], "katex")) 35 | 36 | @testset "Set vars" begin 37 | d = F.PageVars( 38 | "a" => 0.5 => (Real,), 39 | "b" => "hello" => (String, Nothing)) 40 | F.set_vars!(d, ["a"=>"5", "b"=>"nothing"]) 41 | 42 | @test d["a"].first == 5 43 | @test d["b"].first === nothing 44 | 45 | @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\""]) 46 | 47 | @test_throws F.PageVariableError F.set_vars!(d, ["a"=> "sqrt(-1)"]) 48 | 49 | # assigning new variables 50 | 51 | F.set_vars!(d, ["blah"=>"1"]) 52 | @test d["blah"].first == 1 53 | end 54 | 55 | @testset "Def+coms" begin # see #78 56 | st = raw""" 57 | @def title = "blah" 58 | @def hasmath = false 59 | etc 60 | """ 61 | m = F.convert_md(st) 62 | @test F.locvar("title") == "blah" 63 | @test F.locvar("hasmath") == false 64 | end 65 | -------------------------------------------------------------------------------- /test/parser/md-dbb.jl: -------------------------------------------------------------------------------- 1 | @testset "Bad cases" begin 2 | F.def_LOCAL_VARS!() 3 | # Lonely End block 4 | s = """A {{end}}""" 5 | @test_throws F.HTMLBlockError F.convert_html(s) 6 | 7 | # Inbalanced 8 | s = """A {{if a}} B {{if b}} C {{else}} {{end}}""" 9 | @test_throws F.HTMLBlockError F.convert_html(s) 10 | 11 | # Some of the conditions are not bools 12 | F.set_vars!(F.LOCAL_VARS, [ 13 | "a" => "false", 14 | "b" => "false", 15 | "c" => "\"Hello\""]) 16 | s = """A {{if a}} A {{elseif c}} B {{end}}""" 17 | @test_throws F.HTMLBlockError F.convert_html(s) 18 | end 19 | 20 | 21 | @testset "Script" begin 22 | F.def_LOCAL_VARS!() 23 | F.set_var!(F.LOCAL_VARS, "hasmath", true) 24 | s = """ 25 | Hasmath: {{hasmath}} 26 | 27 | 28 | """ 29 | @test isapproxstr(F.convert_html(s), """ 30 | Hasmath: true 31 | 32 | 33 | """) 34 | end 35 | 36 | # issue #482 37 | @testset "div-dbb" begin 38 | Franklin.eval(:(hfun_bar(p) = string(round(sqrt(Meta.parse(p[1])), digits=1)) )) 39 | s = "@@B @@A {{author}} @@\n@@ \n" |> fd2html 40 | @test isapproxstr(s, """ 41 |
THE AUTHOR
42 | """) 43 | s = "**{{author}}**" |> fd2html 44 | @test isapproxstr(s, "

THE AUTHOR

") 45 | s = raw"\style{font-weight:bold;}{ {{author}} }" |> fd2html 46 | @test isapproxstr(s, """ 47 | THE AUTHOR 48 | """) 49 | s = raw"@@bold {{bar 4}} @@" |> fd2html 50 | @test isapproxstr(s, """ 51 |
2.0
52 | """) 53 | end 54 | 55 | @testset "iss#502" begin 56 | s = """ 57 | @def upcoming_release_short = "1.5" 58 | @def upcoming_release_date = "May 28, 2020" 59 | 60 | Blah v{{upcoming_release_short}} and {{upcoming_release_date}}. 61 | """ |> fd2html 62 | @test isapproxstr(s, "

Blah v1.5 and May 28, 2020.

") 63 | end 64 | -------------------------------------------------------------------------------- /test/utils/html.jl: -------------------------------------------------------------------------------- 1 | @testset "misc-html" begin 2 | fs1() 3 | λ = "blah/blah.ext" 4 | set_curpath("pages/cpB/blah.md") 5 | @test F.html_ahref(λ, 1) == "1" 6 | @test F.html_ahref(λ, "bb") == "bb" 7 | @test F.html_ahref_key("cc", "dd") == "dd" 8 | @test F.html_div("dn","ct") == "
ct
" 9 | @test F.html_img("src", "alt") == "\"alt\"" 10 | @test F.html_code("code") == "
code
" 11 | @test F.html_code("code", "lang") == "
code
" 12 | @test F.html_err("blah") == "

// blah //

" 13 | 14 | fs2() 15 | λ = "blah/blah.ext" 16 | set_curpath("cpB/blah.md") 17 | @test F.html_ahref(λ, 1) == "1" 18 | @test F.html_ahref(λ, "bb") == "bb" 19 | @test F.html_ahref_key("cc", "dd") == "dd" 20 | 21 | fs1() 22 | end 23 | 24 | @testset "misc-html 2" begin 25 | h = "
blah
" 26 | @test !F.is_html_escaped(h) 27 | @test F.html_code(h, "html") == """
<div class="foo">blah</div>
""" 28 | he = Markdown.htmlesc(h) 29 | @test F.is_html_escaped(he) 30 | @test F.html_code(h, "html") == F.html_code(h, "html") 31 | end 32 | 33 | @testset "html/hide" begin 34 | c = """ 35 | a=5 36 | b=7 37 | """ 38 | @test F.html_skip_hidden(c, "foo") == c 39 | c = """ 40 | #hideall 41 | a=5 42 | b=7 43 | """ 44 | @test F.html_skip_hidden(c, "julia") == "" 45 | c = """ 46 | a=5 #hide 47 | b=7 48 | """ 49 | @test F.html_skip_hidden(c, "julia") == "b=7" 50 | end 51 | 52 | @testset "html_code" begin 53 | c = """ 54 | using Random 55 | Random.seed!(555) # hide 56 | a = randn() 57 | b = a + 5 58 | """ 59 | @test F.html_code(c, "julia") == 60 | """
using Random
61 |         a = randn()
62 |         b = a + 5
""" 63 | end 64 | -------------------------------------------------------------------------------- /test/converter/html/html_for.jl: -------------------------------------------------------------------------------- 1 | @testset "h-for" begin 2 | F.def_LOCAL_VARS!() 3 | s = """ 4 | @def list = [1,2,3] 5 | """ |> fd2html_td 6 | hs = raw""" 7 | ABC 8 | {{for x in list}} 9 | {{fill x}} 10 | {{end}} 11 | """ 12 | tokens = F.find_tokens(hs, F.HTML_TOKENS, F.HTML_1C_TOKENS) 13 | hblocks, tokens = F.find_all_ocblocks(tokens, F.HTML_OCB) 14 | qblocks = F.qualify_html_hblocks(hblocks) 15 | @test qblocks[1] isa F.HFor 16 | @test qblocks[1].vname == "x" 17 | @test qblocks[1].iname == "list" 18 | @test qblocks[2] isa F.HFun 19 | @test qblocks[3] isa F.HEnd 20 | 21 | content, head, i = F.process_html_for(hs, qblocks, 1) 22 | @test isapproxstr(content, "1 2 3") 23 | end 24 | 25 | @testset "h-for2" begin 26 | F.def_LOCAL_VARS!() 27 | s = """ 28 | @def list = ["path/to/badge1.png", "path/to/badge2.png"] 29 | """ |> fd2html_td 30 | h = raw""" 31 | ABC 32 | {{for x in list}} 33 | {{fill x}} 34 | {{end}} 35 | """ |> F.convert_html 36 | @test isapproxstr(h, """ 37 | ABC 38 | path/to/badge1.png 39 | path/to/badge2.png 40 | """) 41 | end 42 | 43 | @testset "h-for3" begin 44 | F.def_LOCAL_VARS!() 45 | s = """ 46 | @def iter = (("a", 1), ("b", 2), ("c", 3)) 47 | """ |> fd2html_td 48 | h = raw""" 49 | ABC 50 | {{for (n, v) in iter}} 51 | name:{{fill n}} 52 | value:{{fill v}} 53 | {{end}} 54 | """ |> F.convert_html 55 | @test isapproxstr(h, """ 56 | ABC 57 | name:a 58 | value:1 59 | name:b 60 | value:2 61 | name:c 62 | value:3 63 | """) 64 | 65 | s = """ 66 | @def iter2 = ("a"=>10, "b"=>7, "c"=>3) 67 | """ |> fd2html_td 68 | h = raw""" 69 | ABC 70 | {{for (n, v) in iter2}} 71 | name:{{fill n}} 72 | value:{{fill v}} 73 | {{end}} 74 | """ |> F.convert_html 75 | @test isapproxstr(h, """ 76 | ABC 77 | name:a 78 | value:10 79 | name:b 80 | value:7 81 | name:c 82 | value:3 83 | """) 84 | end 85 | -------------------------------------------------------------------------------- /test/utils_file/basic.jl: -------------------------------------------------------------------------------- 1 | # Tests for what happens in the `utils.jl` 2 | 3 | fs2() 4 | write(joinpath("_layout", "head.html"), "") 5 | write(joinpath("_layout", "foot.html"), "") 6 | write(joinpath("_layout", "page_foot.html"), "") 7 | write("config.md", "") 8 | 9 | @testset "utils1" begin 10 | write("utils.jl", """ 11 | x = 5 12 | """) 13 | F.process_utils() 14 | s = """ 15 | {{fill x}} 16 | """ |> fdi 17 | @test isapproxstr(s, "5") 18 | s = """ 19 | {{x}} 20 | """ |> fdi 21 | @test isapproxstr(s, "5") 22 | end 23 | 24 | @testset "utils:hfun" begin 25 | write("utils.jl", """ 26 | hfun_foo() = return "blah" 27 | """) 28 | F.process_utils() 29 | s = """ 30 | {{foo}} 31 | """ |> fdi 32 | @test isapproxstr(s, "blah") 33 | 34 | write("utils.jl", """ 35 | function hfun_bar(vname) 36 | val = locvar(vname[1]) 37 | return round(sqrt(val), digits=2) 38 | end 39 | """) 40 | F.process_utils() 41 | s = """ 42 | @def xx = 25 43 | {{bar xx}} 44 | """ |> fdi 45 | @test isapproxstr(s, "5.0") 46 | end 47 | 48 | @testset "utils:lxfun" begin 49 | write("utils.jl", """ 50 | function lx_foo(lxc::Franklin.LxCom, _) 51 | return uppercase(Franklin.content(lxc.braces[1])) 52 | end 53 | """) 54 | F.process_utils() 55 | s = raw""" 56 | \foo{bar} 57 | """ |> fdi 58 | @test isapproxstr(s, "BAR") 59 | 60 | write("utils.jl", """ 61 | function lx_baz(com, _) 62 | brace_content = Franklin.content(com.braces[1]) 63 | return uppercase(brace_content) 64 | end 65 | """) 66 | F.process_utils() 67 | s = raw""" 68 | \baz{bar} 69 | """ |> fdi 70 | @test isapproxstr(s, "BAR") 71 | end 72 | 73 | # 452 74 | @testset "utils:lxfun2" begin 75 | write("utils.jl", raw""" 76 | function lx_bold(com, _) 77 | text = Franklin.content(com.braces[1]) 78 | return "**$text**" 79 | end 80 | """) 81 | F.process_utils() 82 | s = raw""" 83 | \bold{bar} 84 | """ |> fdi 85 | @test isapproxstr(s, "bar") 86 | end 87 | -------------------------------------------------------------------------------- /src/converter/latex/latex.jl: -------------------------------------------------------------------------------- 1 | """ 2 | $(SIGNATURES) 3 | 4 | Take a `LxCom` object `lxc` and try to resolve it. Provided a definition exists 5 | etc, the definition is plugged in then sent forward to be re-parsed (in case 6 | further latex is present). 7 | """ 8 | function resolve_lxcom(lxc::LxCom, lxdefs::Vector{LxDef}; 9 | inmath::Bool=false)::String 10 | # retrieve the definition the command points to 11 | lxd = getdef(lxc) 12 | # it will be `nothing` in math mode, let KaTeX have it 13 | if lxd === nothing 14 | name = getname(lxc) # `\\cite` -> `cite` 15 | fun = Symbol("lx_" * name) 16 | if isdefined(Main, :Utils) && isdefined(Main.Utils, fun) 17 | raw = Core.eval(Main.Utils, :($fun($lxc, $lxdefs))) 18 | return reprocess(raw, lxdefs) 19 | else 20 | return lxc.ss 21 | end 22 | end 23 | # otherwise it may be 24 | # -> empty, in which case try to find a specific internal definition or 25 | # return an empty string (see `commands.jl`) 26 | # -> non-empty, in which case just apply that. 27 | if isempty(lxd) 28 | # see if a function `lx_name` exists 29 | name = getname(lxc) # `\\cite` -> `cite` 30 | fun = Symbol("lx_" * name) 31 | if isdefined(Franklin, fun) 32 | # apply that function 33 | return eval(:($fun($lxc, $lxdefs))) 34 | else 35 | return "" 36 | end 37 | end 38 | # non-empty case, take the definition and iteratively replace any `#...` 39 | partial = lxd 40 | for (i, brace) in enumerate(lxc.braces) 41 | cont = stent(brace) 42 | # space-sensitive 'unsafe' one 43 | partial = replace(partial, "!#$i" => cont) 44 | # space-insensitive 'safe' one (e.g. `\mathbb#1`) 45 | partial = replace(partial, "#$i" => " " * cont) 46 | end 47 | # if 'inmath' surround accordingly so that this information is preserved 48 | partial = ifelse(inmath, mathenv(partial), partial) 49 | # reprocess 50 | return reprocess(partial, lxdefs) 51 | end 52 | 53 | """Convenience function to take a string and re-parse it.""" 54 | function reprocess(s::AS, lxdefs::Vector{LxDef}) 55 | r = convert_md(s, lxdefs; 56 | isrecursive=true, isconfig=false, has_mddefs=false) 57 | return r 58 | end 59 | -------------------------------------------------------------------------------- /src/parser/latex/tokens.jl: -------------------------------------------------------------------------------- 1 | """ 2 | LX_TOKENS 3 | 4 | List of names of latex tokens. (See markdown tokens) 5 | """ 6 | const LX_TOKENS = (:LX_BRACE_OPEN, :LX_BRACE_CLOSE, :LX_COMMAND) 7 | 8 | 9 | """ 10 | $(TYPEDEF) 11 | 12 | Structure to keep track of the definition of a latex command declared via a 13 | `\newcommand{\name}[narg]{def}`. 14 | """ 15 | struct LxDef 16 | name::String 17 | narg::Int 18 | def ::AS 19 | # location of the definition 20 | from::Int 21 | to ::Int 22 | end 23 | # if offset unspecified, start from basically -∞ (configs etc) 24 | function LxDef(name::String, narg::Int, def::AS) 25 | o = FD_ENV[:OFFSET_LXDEFS] += 5 # precise offset doesn't matter, jus 26 | LxDef(name, narg, def, o, o + 3) # just forward a bit 27 | end 28 | 29 | from(lxd::LxDef) = lxd.from 30 | to(lxd::LxDef) = lxd.to 31 | 32 | 33 | """ 34 | pastdef(λ) 35 | 36 | Convenience function to return a copy of a definition indicated as having 37 | already been earlier in the context i.e.: earlier than any other definition 38 | appearing in the current chunk. 39 | """ 40 | pastdef(λ::LxDef) = LxDef(λ.name, λ.narg, λ.def) 41 | 42 | """ 43 | $(TYPEDEF) 44 | 45 | A `LxCom` has a similar content as a `Block`, with the addition of the definition and a vector of brace blocks. 46 | """ 47 | struct LxCom <: AbstractBlock 48 | ss ::SubString # \\command 49 | lxdef ::Union{Nothing,Ref{LxDef}} # definition of the command 50 | braces::Vector{OCBlock} # relevant {...} with the command 51 | end 52 | LxCom(ss, def) = LxCom(ss, def, Vector{OCBlock}()) 53 | from(lxc::LxCom) = from(lxc.ss) 54 | to(lxc::LxCom) = to(lxc.ss) 55 | 56 | 57 | """ 58 | For a given `LxCom`, retrieve the definition attached to the corresponding 59 | `LxDef` via the reference. 60 | """ 61 | function getdef(lxc::LxCom)::Union{Nothing,AS} 62 | if isnothing(lxc.lxdef) 63 | return nothing 64 | end 65 | return getindex(lxc.lxdef).def 66 | end 67 | 68 | """ 69 | For a given `LxCom`, retrieve the name of the command via the reference. 70 | Example: `\\cite` --> `cite`. 71 | """ 72 | function getname(lxc::LxCom)::String 73 | if isnothing(lxc.lxdef) 74 | s = String(lxc.ss) 75 | j = findfirst('{', s) 76 | return lxc.ss[2:prevind(s, j)] 77 | end 78 | return String(getindex(lxc.lxdef).name)[2:end] 79 | end 80 | # XXX if nothing lxc.lxdef then extract from lxc.ss but extract before {} 81 | -------------------------------------------------------------------------------- /test/eval/run.jl: -------------------------------------------------------------------------------- 1 | @testset "parse_code" begin 2 | c = """ 3 | a = 7 4 | println(a); a^2 5 | """ 6 | exs = F.parse_code(c) 7 | @test exs[1] == :(a=7) 8 | @test exs[2].args[1] == :(println(a)) 9 | @test exs[2].args[2] == :(a ^ 2) 10 | # invalid code is fine 11 | c = """ 12 | a = 7 13 | f = foo(a = 3 14 | """ 15 | exs = F.parse_code(c) 16 | @test exs[1] == :(a = 7) 17 | @test exs[2].head == :incomplete 18 | @test exs[2].args[1] == "incomplete: premature end of input" 19 | # empty code 20 | c = "" 21 | exs = F.parse_code(c) 22 | @test isempty(exs) 23 | end 24 | 25 | @testset "run_code" begin 26 | mn = F.modulename("foo/path.md") 27 | mod = F.newmodule(mn) 28 | junk = tempname() 29 | 30 | # empty code 31 | c = "" 32 | @test isnothing(F.run_code(mod, c, junk)) 33 | 34 | # code with no print 35 | c = """ 36 | const a = 5 37 | a^2 38 | """ 39 | r = F.run_code(mod, c, junk) 40 | @test r == 25 41 | @test isempty(read(junk, String)) 42 | 43 | # code with print 44 | c = """ 45 | using Random 46 | Random.seed!(555) 47 | println("hello") 48 | b = randn() 49 | b > 0 50 | """ 51 | r = F.run_code(mod, c, junk) 52 | 53 | @test r == false 54 | @test read(junk, String) == "hello\n" 55 | 56 | # code with show 57 | c = """ 58 | x = 5 59 | @show x 60 | y = 7; 61 | """ 62 | r = F.run_code(mod, c, junk) 63 | @test isnothing(r) 64 | @test read(junk, String) == "x = 5\n" 65 | 66 | # code with errorr 67 | c = """ 68 | e = 0 69 | a = sqrt(-1) 70 | b = 7 71 | """ 72 | @test (@test_logs (:warn, "There was an error of type DomainError running the code.") F.run_code(mod, c, junk)) === nothing 73 | if VERSION >= v"1.2" 74 | @test read(junk, String) == """DomainError with -1.0: 75 | sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)). 76 | """ 77 | end 78 | end 79 | 80 | @testset "i462" begin 81 | s = raw""" 82 | A 83 | ```julia:ex 84 | 1 # hide 85 | ``` 86 | \show{ex} 87 | B""" |> fd2html_td 88 | @test isapproxstr(s, """ 89 |

A

1
B

90 | """) 91 | s = raw""" 92 | A 93 | ```julia:ex 94 | "hello" # hide 95 | ``` 96 | \show{ex} 97 | B""" |> fd2html_td 98 | @test isapproxstr(s, """ 99 |

A

"hello"
B

100 | """) 101 | end 102 | -------------------------------------------------------------------------------- /test/converter/md/md_defs.jl: -------------------------------------------------------------------------------- 1 | @testset "preprocess" begin 2 | s = """ 3 | @def z = [1,2,3, 4 | 4,5,6] 5 | """ 6 | tokens = F.find_tokens(s, F.MD_TOKENS, F.MD_1C_TOKENS) 7 | F.find_indented_blocks!(tokens, s) 8 | 9 | @test tokens[1].name == :MD_DEF_OPEN 10 | @test tokens[2].name == :LR_INDENT 11 | @test tokens[3].name == :LINE_RETURN 12 | @test tokens[4].name == :EOS 13 | 14 | F.preprocess_candidate_mddefs!(tokens) 15 | 16 | @test tokens[1].name == :MD_DEF_OPEN 17 | @test tokens[2].name == :LINE_RETURN 18 | @test tokens[3].name == :EOS 19 | 20 | blocks, = F.find_all_ocblocks(tokens, F.MD_OCB_ALL) 21 | 22 | content = F.content.(blocks) .|> String 23 | @test isapproxstr(content[1], "z = [1,2,3,4,5,6]") 24 | 25 | s = """ 26 | @def z1 = [1,2,3, 27 | 4,5,6] 28 | @def z2 = 3 29 | @def z3 = [1, 30 | 2, 31 | 3] 32 | """ 33 | tokens = F.find_tokens(s, F.MD_TOKENS, F.MD_1C_TOKENS) 34 | F.find_indented_blocks!(tokens, s) 35 | seqtok = (:MD_DEF_OPEN, :LR_INDENT, :LINE_RETURN, 36 | :MD_DEF_OPEN, :LINE_RETURN, 37 | :MD_DEF_OPEN, :LR_INDENT, :LR_INDENT, :LINE_RETURN, 38 | :EOS) 39 | for i in 1:length(tokens) 40 | @test tokens[i].name == seqtok[i] 41 | end 42 | 43 | F.preprocess_candidate_mddefs!(tokens) 44 | seqtok = (:MD_DEF_OPEN, :LINE_RETURN, 45 | :MD_DEF_OPEN, :LINE_RETURN, 46 | :MD_DEF_OPEN, :LINE_RETURN, 47 | :EOS) 48 | for i in 1:length(tokens) 49 | @test tokens[i].name == seqtok[i] 50 | end 51 | 52 | blocks, = F.find_all_ocblocks(tokens, F.MD_OCB_ALL) 53 | content = F.content.(blocks) .|> String 54 | @test isapproxstr(content[1], "z1=[1,2,3,4,5,6]") 55 | @test isapproxstr(content[2], "z2=3") 56 | @test isapproxstr(content[3], "z3=[1,2,3]") 57 | end 58 | 59 | 60 | @testset "mddefs1" begin 61 | F.def_LOCAL_VARS!() 62 | s = """ 63 | @def x = 5 64 | """ |> fd2html_td 65 | @test F.locvar("x") == 5 66 | s = """ 67 | @def x = "hello"; 68 | """ |> fd2html_td 69 | @test F.locvar("x") == "hello" 70 | s = """ 71 | @def x = "hello" 72 | @def y = pi 73 | """ |> fd2html_td 74 | @test F.locvar("x") == "hello" 75 | @test F.locvar("y") == pi 76 | s = """ 77 | @def z = [1,2,3, 78 | 4,5,6] 79 | A 80 | """ |> fd2html_td 81 | @test F.locvar("z") == collect(1:6) 82 | s = """ 83 | @def s = \"\"\"foo 84 | bar 85 | baz etc\"\"\" 86 | @def s2 = "nothing" 87 | """ |> fd2html_td 88 | @test isapproxstr(F.locvar("s"), "foo bar baz etc") 89 | @test isapproxstr(F.locvar("s2"), "nothing") 90 | @test F.locvar("foobar") === nothing 91 | @test F.globvar("foobar") === nothing 92 | end 93 | -------------------------------------------------------------------------------- /src/parser/html/blocks.jl: -------------------------------------------------------------------------------- 1 | """ 2 | $(SIGNATURES) 3 | 4 | Given `{{ ... }}` blocks, identify what kind of blocks they are and return a 5 | vector of qualified blocks of type `AbstractBlock`. 6 | """ 7 | function qualify_html_hblocks(blocks::Vector{OCBlock})::Vector{AbstractBlock} 8 | 9 | qb = Vector{AbstractBlock}(undef, length(blocks)) 10 | 11 | # TODO improve this (if there are many blocks this would be slow) 12 | for (i, β) ∈ enumerate(blocks) 13 | # if block {{ if v }} 14 | m = match(HBLOCK_IF_PAT, β.ss) 15 | isnothing(m) || (qb[i] = HIf(β.ss, m.captures[1]); continue) 16 | # else block {{ else }} 17 | m = match(HBLOCK_ELSE_PAT, β.ss) 18 | isnothing(m) || (qb[i] = HElse(β.ss); continue) 19 | # else if block {{ elseif v }} 20 | m = match(HBLOCK_ELSEIF_PAT, β.ss) 21 | isnothing(m) || (qb[i] = HElseIf(β.ss, m.captures[1]); continue) 22 | # end block {{ end }} 23 | m = match(HBLOCK_END_PAT, β.ss) 24 | isnothing(m) || (qb[i] = HEnd(β.ss); continue) 25 | # --- 26 | # isdef block 27 | m = match(HBLOCK_ISDEF_PAT, β.ss) 28 | isnothing(m) || (qb[i] = HIsDef(β.ss, m.captures[1]); continue) 29 | # ifndef block 30 | m = match(HBLOCK_ISNOTDEF_PAT, β.ss) 31 | isnothing(m) || (qb[i] = HIsNotDef(β.ss, m.captures[1]); continue) 32 | # --- 33 | # ispage block 34 | m = match(HBLOCK_ISPAGE_PAT, β.ss) 35 | isnothing(m) || (qb[i] = HIsPage(β.ss, split(m.captures[1])); continue) 36 | # isnotpage block 37 | m = match(HBLOCK_ISNOTPAGE_PAT, β.ss) 38 | isnothing(m) || (qb[i] = HIsNotPage(β.ss, split(m.captures[1])); continue) 39 | # --- 40 | # for block 41 | m = match(HBLOCK_FOR_PAT, β.ss) 42 | if !isnothing(m) 43 | v, iter = m.captures 44 | check_for_pat(v) 45 | qb[i] = HFor(β.ss, v, iter); 46 | continue 47 | end 48 | # --- 49 | # function block {{ fname v1 v2 ... }} 50 | m = match(HBLOCK_FUN_PAT, β.ss) 51 | if !isnothing(m) 52 | if isnothing(m.captures[2]) || isempty(strip(m.captures[2])) 53 | ps = String[] 54 | else 55 | ps = split(m.captures[2]) 56 | end 57 | qb[i] = HFun(β.ss, m.captures[1], ps) 58 | continue 59 | end 60 | # --- 61 | throw(HTMLBlockError("I found a HBlock that did not match anything, " * 62 | "verify '$(β.ss)'")) 63 | end 64 | return qb 65 | end 66 | 67 | 68 | """Blocks that can open a conditional block.""" 69 | const HTML_OPEN_COND = Union{HIf,HIsDef,HIsNotDef,HIsPage,HIsNotPage} 70 | 71 | 72 | """ 73 | $SIGNATURES 74 | 75 | Internal function to balance conditional blocks. See [`process_html_qblocks`](@ref). 76 | """ 77 | hbalance(::HTML_OPEN_COND) = 1 78 | hbalance(::HEnd) = -1 79 | hbalance(::AbstractBlock) = 0 80 | -------------------------------------------------------------------------------- /test/eval/io.jl: -------------------------------------------------------------------------------- 1 | fs1() 2 | 3 | @testset "unixify" begin 4 | @test F.unixify("") == "/" 5 | @test F.unixify("blah.txt") == "blah.txt" 6 | @test F.unixify("blah/") == "blah/" 7 | @test F.unixify("foo/bar") == "foo/bar/" 8 | end 9 | 10 | @testset "join_rpath" begin 11 | @test F.join_rpath("blah/blah") == joinpath("blah", "blah") 12 | end 13 | 14 | @testset "parse_rpath" begin 15 | F.PATHS[:folder] = "fld" 16 | # /[path] 17 | @test_throws F.RelativePathError F.parse_rpath("/") 18 | @test F.parse_rpath("/a") == "/a" 19 | @test F.parse_rpath("/a/b") == "/a/b" 20 | @test F.parse_rpath("/a/b", canonical=true) == joinpath("fld", "a", "b") 21 | @test F.parse_rpath("/a/../b", canonical=true) == joinpath("fld", "b") 22 | 23 | # ./[path] 24 | set_curpath(joinpath("pages", "pg1.md")) 25 | F.PATHS[:assets] = joinpath("fld", "assets") 26 | @test_throws F.RelativePathError F.parse_rpath("./") 27 | @test F.parse_rpath("./a") == "/assets/pages/pg1/a" 28 | @test F.parse_rpath("./a/b") == "/assets/pages/pg1/a/b" 29 | @test F.parse_rpath("./a/b", canonical=true) == joinpath("fld", "assets", "pages", "pg1", "a", "b") 30 | 31 | # [path] 32 | @test_throws F.RelativePathError F.parse_rpath("") 33 | @test F.parse_rpath("blah") == "/assets/blah" 34 | @test F.parse_rpath("blah", code=true) == "/assets/pages/pg1/code/blah" 35 | @test F.parse_rpath("blah", canonical=true) == joinpath("fld", "assets", "blah") 36 | end 37 | 38 | @testset "resolve_rpath" begin 39 | root = F.PATHS[:folder] = mktempdir() 40 | ass = F.PATHS[:assets] = joinpath(root, "assets") 41 | mkdir(ass) 42 | write(joinpath(ass, "p1.jl"), "a = 5") 43 | write(joinpath(ass, "p2.png"), "gibberish") 44 | 45 | fp, d, fn = F.resolve_rpath("/assets/p1", "julia") 46 | @test fp == joinpath(ass, "p1.jl") 47 | @test d == ass 48 | @test fn == "p1" 49 | 50 | fp, d, fn = F.resolve_rpath("/assets/p2") 51 | @test fp == joinpath(ass, "p2.png") 52 | @test d == ass 53 | @test fn == "p2" 54 | 55 | @test_throws F.FileNotFoundError F.resolve_rpath("/assets/foo") 56 | @test_throws F.FileNotFoundError F.resolve_rpath("foo") 57 | @test_throws F.FileNotFoundError F.resolve_rpath("foo", "julia") 58 | end 59 | 60 | @testset "form_cpaths" begin 61 | root = F.PATHS[:folder] = mktempdir() 62 | ass = F.PATHS[:assets] = joinpath(root, "assets") 63 | mkdir(ass) 64 | curpath = set_curpath(joinpath("pages", "pg1.md")) 65 | 66 | write(joinpath(ass, "p1.jl"), "a = 5") 67 | write(joinpath(ass, "p2.png"), "gibberish") 68 | 69 | sp, sd, sn, od, op, rp = F.form_codepaths("p1") 70 | @test sp == joinpath(ass, splitext(curpath)[1], "code", "p1.jl") 71 | @test sd == joinpath(ass, splitext(curpath)[1], "code") 72 | @test sn == "p1.jl" 73 | @test od == joinpath(ass, splitext(curpath)[1], "code", "output") 74 | @test op == joinpath(od, "p1.out") 75 | @test rp == joinpath(od, "p1.res") 76 | end 77 | -------------------------------------------------------------------------------- /src/converter/latex/hyperrefs.jl: -------------------------------------------------------------------------------- 1 | """ 2 | PAGE_EQREFS 3 | 4 | Dictionary to keep track of equations that are labelled on a page to allow 5 | references within the page. 6 | """ 7 | const PAGE_EQREFS = LittleDict{String, Int}() 8 | 9 | """ 10 | PAGE_EQREFS_COUNTER 11 | 12 | Counter to keep track of equation numbers as they appear along the page, this 13 | helps with equation referencing. (The `_XC0q` is just a random string to avoid 14 | clashes). 15 | """ 16 | const PAGE_EQREFS_COUNTER = "COUNTER_XC0q" 17 | 18 | """ 19 | $(SIGNATURES) 20 | 21 | Reset the PAGE_EQREFS dictionary. 22 | """ 23 | function def_PAGE_EQREFS!() 24 | empty!(PAGE_EQREFS) 25 | PAGE_EQREFS[PAGE_EQREFS_COUNTER] = 0 26 | return nothing 27 | end 28 | 29 | """ 30 | PAGE_BIBREFS 31 | 32 | Dictionary to keep track of bibliographical references on a page to allow 33 | citation within the page. 34 | """ 35 | const PAGE_BIBREFS = LittleDict{String, String}() 36 | 37 | """ 38 | $(SIGNATURES) 39 | 40 | Reset the PAGE_BIBREFS dictionary. 41 | """ 42 | def_PAGE_BIBREFS!() = (empty!(PAGE_BIBREFS); nothing) 43 | 44 | 45 | """ 46 | $(SIGNATURES) 47 | 48 | Given a latex command such as `\\eqref{abc}`, hash the reference (here `abc`), 49 | check if the given dictionary `d` has an entry corresponding to that hash and 50 | return the appropriate HTML for it. 51 | """ 52 | function form_href(lxc::LxCom, dname::String; 53 | parens="("=>")", class="href")::String 54 | cont = content(lxc.braces[1]) # "r1, r2, r3" 55 | refs = strip.(split(cont, ",")) # ["r1", "r2", "r3"] 56 | names = refstring.(refs) 57 | nkeys = length(names) 58 | # construct the partial link with appropriate parens, it will be 59 | # resolved at the second pass (HTML pass) whence the introduction of {{..}} 60 | # inner will be "{{href $dn $hr1}}, {{href $dn $hr2}}, {{href $dn $hr3}}" 61 | # where $hr1 is the hash of r1 etc. 62 | inner = prod("{{href $dname $name}}$(ifelse(i < nkeys, ", ", ""))" 63 | for (i, name) ∈ enumerate(names)) 64 | # encapsulate in a span for potential css-styling 65 | return html_span(class, parens.first * inner * parens.second) 66 | end 67 | 68 | lx_eqref(lxc::LxCom, _) = form_href(lxc, "EQR"; class="eqref") 69 | lx_cite(lxc::LxCom, _) = form_href(lxc, "BIBR"; parens=""=>"", class="bibref") 70 | lx_citet(lxc::LxCom, _) = form_href(lxc, "BIBR"; parens=""=>"", class="bibref") 71 | lx_citep(lxc::LxCom, _) = form_href(lxc, "BIBR"; class="bibref") 72 | 73 | function lx_label(lxc::LxCom, _) 74 | refs = content(lxc.braces[1]) |> strip |> refstring 75 | return "" 76 | end 77 | 78 | function lx_biblabel(lxc::LxCom, _)::String 79 | name = refstring(stent(lxc.braces[1])) 80 | PAGE_BIBREFS[name] = content(lxc.braces[2]) 81 | return "" 82 | end 83 | 84 | function lx_toc(::LxCom, _) 85 | minlevel = locvar("mintoclevel") 86 | maxlevel = locvar("maxtoclevel") 87 | return "{{toc $minlevel $maxlevel}}" 88 | end 89 | -------------------------------------------------------------------------------- /src/build.jl: -------------------------------------------------------------------------------- 1 | const PY = begin 2 | if "PYTHON3" ∈ keys(ENV) 3 | ENV["PYTHON3"] 4 | else 5 | if Sys.iswindows() 6 | "py -3" 7 | else 8 | "python3" 9 | end 10 | end 11 | end 12 | const PIP = begin 13 | if "PIP3" ∈ keys(ENV) 14 | ENV["PIP3"] 15 | else 16 | if Sys.iswindows() 17 | "py -3 -m pip" 18 | else 19 | "pip3" 20 | end 21 | end 22 | end 23 | const NODE = begin 24 | if "NODE" ∈ keys(ENV) 25 | ENV["NODE"] 26 | else 27 | NodeJS.nodejs_cmd() 28 | end 29 | end 30 | 31 | shell_try(com)::Bool = try success(com); catch; false; end 32 | 33 | #= 34 | Pre-rendering 35 | - In order to prerender KaTeX, the only thing that is required is `node` and then we can just 36 | require `katex.min.js` 37 | - For highlights, we need `node` and also to have the `highlight.js` installed via `npm`. 38 | =# 39 | const FD_CAN_PRERENDER = shell_try(`$NODE -v`) 40 | const FD_CAN_HIGHLIGHT = shell_try(`$NODE -e "require('highlight.js')"`) 41 | 42 | #= 43 | Minification 44 | - We use `css_html_js_minify`. To use it, you need python3 and to install it. 45 | - Here we check there is python3, and pip3, and then if we fail to import, we try to 46 | use pip3 to install it. 47 | =# 48 | const FD_HAS_PY3 = shell_try(`$([e for e in split(PY)]) -V`) 49 | const FD_HAS_PIP3 = shell_try(`$([e for e in split(PIP)]) -V`) 50 | const FD_CAN_MINIFY = FD_HAS_PY3 && FD_HAS_PIP3 && 51 | (success(`$([e for e in split(PY)]) -m "import css_html_js_minify"`) || 52 | success(`$([e for e in split(PIP)]) install css_html_js_minify`)) 53 | #= 54 | Information to the user 55 | - what Franklin couldn't find 56 | - that the user should do a `build` step after installing 57 | =# 58 | FD_CAN_HIGHLIGHT || begin 59 | FD_CAN_PRERENDER || begin 60 | println("""✘ Couldn't find node.js (`$NODE -v` failed). 61 | → It is required for pre-rendering KaTeX and highlight.js but is not necessary to run Franklin (cf docs).""") 62 | end 63 | println("""✘ Couldn't find highlight.js (`$NODE -e "require('highlight.js')"` failed). 64 | → It is required for pre-rendering highlight.js but is not necessary to run Franklin (cf docs).""") 65 | end 66 | 67 | FD_CAN_MINIFY || begin 68 | if FD_HAS_PY3 69 | println("✘ Couldn't find css_html_js_minify (`$([e for e in split(PY)]) -m \"import css_html_js_minify\"` failed).\n" * 70 | """→ It is required for minification but is not necessary to run Franklin (cf docs).""") 71 | else 72 | println("""✘ Couldn't find python3 (`$([e for e in split(PY)]) -V` failed). 73 | → It is required for minification but not necessary to run Franklin (cf docs).""") 74 | end 75 | end 76 | 77 | all((FD_CAN_HIGHLIGHT, FD_CAN_PRERENDER, FD_CAN_MINIFY, FD_HAS_PY3, FD_HAS_PIP3)) || begin 78 | println("→ After installing any missing component, please re-build the package (cf docs).") 79 | end 80 | -------------------------------------------------------------------------------- /test/eval/codeblock.jl: -------------------------------------------------------------------------------- 1 | @testset "parse fenced" begin 2 | s = """ 3 | A 4 | ```julia 5 | using Random 6 | Random.seed!(55) # hide 7 | a = randn() 8 | ``` 9 | B 10 | """ 11 | css = SubString(s, 3, 63) 12 | lang, rpath, code = F.parse_fenced_block(css) 13 | @test lang == "julia" 14 | @test rpath === nothing 15 | @test code == 16 | """using Random 17 | Random.seed!(55) # hide 18 | a = randn()""" 19 | s = """ 20 | A 21 | ```julia:ex1 22 | using Random 23 | Random.seed!(55) # hide 24 | a = randn() 25 | ``` 26 | B 27 | """ 28 | css = SubString(s, 3, 67) 29 | lang, rpath, code = F.parse_fenced_block(css) 30 | @test lang == "julia" 31 | @test rpath == "ex1" 32 | @test code == 33 | """using Random 34 | Random.seed!(55) # hide 35 | a = randn()""" 36 | 37 | # more cases to test REGEX_CODE 38 | rx3 = F.CODE_3_PAT 39 | rx5 = F.CODE_5_PAT 40 | 41 | s = "```julia:ex1 A```" 42 | m = match(rx3, s) 43 | @test m.captures[2] == ":ex1" 44 | s = "```julia:ex1_b2 A```" 45 | m = match(rx3, s) 46 | @test m.captures[2] == ":ex1_b2" 47 | s = "```julia:ex1_b2-33 A```" 48 | m = match(rx3, s) 49 | @test m.captures[2] == ":ex1_b2-33" 50 | s = "```julia:./ex1/v1_b2 A```" 51 | m = match(rx3, s) 52 | @test m.captures[2] == ":./ex1/v1_b2" 53 | s = "```julia:./ex1/v1_b2.f99 A```" 54 | m = match(rx3, s) 55 | @test m.captures[2] == ":./ex1/v1_b2.f99" 56 | 57 | s = "`````julia:./ex1/v1_b2.f99 A`````" 58 | m = match(rx5, s) 59 | @test m.captures[2] == ":./ex1/v1_b2.f99" 60 | end 61 | 62 | @testset "resolve code" begin 63 | # no eval 64 | c = SubString( 65 | """```julia 66 | a = 5 67 | b = 7 68 | ```""") 69 | @test F.resolve_code_block(c) == 70 | """
a = 5
 71 |         b = 7
""" 72 | # not julia code 73 | c = SubString( 74 | """```python:ex 75 | a = 5 76 | b = 7 77 | ```""") 78 | @test @test_logs (:warn, "Evaluation of non-Julia code blocks is not yet supported.") F.resolve_code_block(c) == 79 | """
a = 5
 80 |         b = 7
""" 81 | 82 | # should eval 83 | bak = pwd() 84 | tmp = mktempdir() 85 | begin 86 | cd(tmp) 87 | set_curpath("index.md") 88 | F.FOLDER_PATH[] = tmp 89 | F.def_LOCAL_VARS!() 90 | F.set_paths!() 91 | c = SubString( 92 | """```julia:ex 93 | a = 5 94 | b = 7 95 | ```""") 96 | r = F.resolve_code_block(c) 97 | @test r == 98 | """
a = 5
 99 |             b = 7
""" 100 | end 101 | cd(bak) 102 | end 103 | -------------------------------------------------------------------------------- /src/converter/html/link_fixer.jl: -------------------------------------------------------------------------------- 1 | """ 2 | $(SIGNATURES) 3 | 4 | Direct inline-style links are properly processed by Julia's Markdown processor but not: 5 | 6 | * `[link title][some reference]` and later `[some reference]: http://www.reddit.com` 7 | * `[link title]` and later `[link title]: https://www.mozilla.org` 8 | * (we don't either) `[link title](https://www.google.com "Google's Homepage")` 9 | """ 10 | function find_and_fix_md_links(hs::String)::String 11 | # 1. find all occurences of -- [...]: link 12 | m_link_refs = collect(eachmatch(ESC_LINK_PAT, hs)) 13 | 14 | # recuperate the appropriate id which has a chance to match def_names 15 | ref_names = [ 16 | # no second bracket or empty second bracket ? 17 | # >> true then the id is in the first bracket A --> [id] or [id][] 18 | # >> false then the id is in the second bracket B --> [...][id] 19 | ifelse(isnothing(ref.captures[3]) || isempty(ref.captures[3]), 20 | ref.captures[2], # A. first bracket 21 | ref.captures[3]) # B. second bracket 22 | for ref in m_link_refs] 23 | 24 | # reconstruct the text 25 | h = IOBuffer() 26 | head = 1 27 | i = 0 28 | for (m, refn) in zip(m_link_refs, ref_names) 29 | # write what's before 30 | (head < m.offset) && write(h, subs(hs, head, prevind(hs, m.offset))) 31 | # 32 | def = get(PAGE_LINK_DEFS, refn) do 33 | "" 34 | end 35 | if isempty(def) 36 | # no def found --> just leave it as it was 37 | write(h, m.match) 38 | else 39 | if !isnothing(m.captures[1]) 40 | # CASE: ![alt][id] --> image 41 | write(h, html_img(def, refn)) 42 | else 43 | # It's a link 44 | if isnothing(m.captures[3]) || isempty(m.captures[3]) 45 | # CASE: [id] or [id][] the id is also the link text 46 | write(h, html_ahref(def, refn)) 47 | else 48 | # It's got a second, non-empty bracket 49 | # CASE: [name][id] 50 | name = m.captures[2] 51 | write(h, html_ahref(def, name)) 52 | end 53 | end 54 | end 55 | # move the head after the match 56 | head = nextind(hs, m.offset + lastindex(m.match) - 1) 57 | end 58 | strlen = lastindex(hs) 59 | (head ≤ strlen) && write(h, subs(hs, head, strlen)) 60 | 61 | return String(take!(h)) 62 | end 63 | 64 | 65 | """ 66 | $(SIGNATURES) 67 | 68 | for a project website, for instance `username.github.io/project/` all paths 69 | should eventually be pre-prended with `/project/`. This would happen just 70 | before you publish the website. 71 | """ 72 | function fix_links(pg::String)::String 73 | pp = strip(GLOBAL_VARS["prepath"].first, '/') 74 | ss = SubstitutionString("\\1=\"/$(pp)/") 75 | return replace(pg, r"(src|href|formaction)\s*?=\s*?\"\/" => ss) 76 | end 77 | -------------------------------------------------------------------------------- /src/eval/literate.jl: -------------------------------------------------------------------------------- 1 | const LITERATE_JULIA_FENCE = "```julia" 2 | const LITERATE_JULIA_FENCE_L = length(LITERATE_JULIA_FENCE) 3 | const LITERATE_JULIA_FENCE_R = Regex(LITERATE_JULIA_FENCE) 4 | 5 | """ 6 | $SIGNATURES 7 | 8 | Take a markdown string generated by literate and post-process it to number each 9 | code block and mark them as eval-ed ones. 10 | """ 11 | function literate_post_process(s::String)::String 12 | isempty(s) && return s 13 | em = eachmatch(LITERATE_JULIA_FENCE_R, s) 14 | buf = IOBuffer() 15 | write(buf, "\n") 16 | head = 1 17 | c = 1 18 | for m in em 19 | write(buf, SubString(s, head, prevind(s, m.offset))) 20 | write(buf, "```julia:ex$c\n") 21 | head = nextind(s, m.offset + LITERATE_JULIA_FENCE_L) 22 | c += 1 23 | end 24 | lis = lastindex(s) 25 | head < lis && write(buf, SubString(s, head, lis)) 26 | return String(take!(buf)) 27 | end 28 | 29 | 30 | """ 31 | $SIGNATURES 32 | 33 | Take a Literate.jl script and transform it into a Franklin-Markdown file. 34 | """ 35 | function literate_to_franklin(rpath::AS)::Tuple{String,Bool} 36 | startswith(rpath, "/") || throw( 37 | LiterateRelativePathError("Literate expects a path starting with '/'")) 38 | # rpath is of the form "/scripts/[path/]tutorial[.jl]" 39 | # split it so that when recombining it will lead to valid path inc windows 40 | srpath = split(rpath, '/')[2:end] # discard empty [1] since starts with "/" 41 | fpath = joinpath(PATHS[:folder], srpath...) 42 | # append `.jl` if required 43 | endswith(fpath, ".jl") || (fpath *= ".jl") 44 | if !isfile(fpath) 45 | @warn "File not found when trying to convert a literate file ($fpath)." 46 | return "", true 47 | end 48 | if FD_ENV[:STRUCTURE] < v"0.2" 49 | outpath = joinpath(PATHS[:assets], "literate", 50 | srpath[2:end-1]...) 51 | else 52 | outpath = joinpath(PATHS[:site], "assets", "literate", 53 | srpath[2:end-1]...) 54 | end 55 | isdir(outpath) || mkpath(outpath) 56 | # retrieve the file name 57 | fname = splitext(splitdir(fpath)[2])[1] 58 | spath = joinpath(outpath, fname * "_script.jl") 59 | prev = "" 60 | if isfile(spath) 61 | prev = read(spath, String) 62 | end 63 | # don't show Literate's infos 64 | Logging.disable_logging(Logging.LogLevel(Logging.Info)) 65 | # >> output the markdown 66 | Literate.markdown(fpath, outpath; documenter=false, 67 | postprocess=literate_post_process, credit=false) 68 | # >> output the script 69 | Literate.script(fpath, outpath; documenter=false, 70 | postprocess=s->(MESSAGE_FILE_GEN_LIT * s), 71 | name=fname * "_script", credit=false) 72 | # bring back logging 73 | Logging.disable_logging(Logging.LogLevel(Logging.Debug)) 74 | # see if things have changed 75 | haschanged = (read(spath, String) != prev) 76 | # return path to md file 77 | return joinpath(outpath, fname * ".md"), haschanged 78 | end 79 | -------------------------------------------------------------------------------- /test/eval/io_fs2.jl: -------------------------------------------------------------------------------- 1 | fs2() 2 | 3 | @testset "unixify" begin 4 | @test F.unixify("") == "/" 5 | @test F.unixify("blah.txt") == "blah.txt" 6 | @test F.unixify("blah/") == "blah/" 7 | @test F.unixify("foo/bar") == "foo/bar/" 8 | end 9 | 10 | @testset "join_rpath" begin 11 | @test F.join_rpath("blah/blah") == joinpath("blah", "blah") 12 | end 13 | 14 | @testset "parse_rpath" begin 15 | F.PATHS[:folder] = "fld" 16 | # /[path] 17 | @test_throws F.RelativePathError F.parse_rpath("/") 18 | @test F.parse_rpath("/a") == "/a" 19 | @test F.parse_rpath("/a/b") == "/a/b" 20 | @test F.parse_rpath("/a/b", canonical=true) == joinpath(F.PATHS[:site], "a", "b") 21 | @test F.parse_rpath("/a/../b", canonical=true) == joinpath(F.PATHS[:site], "b") 22 | 23 | # ./[path] 24 | set_curpath("pg1.md") 25 | @test_throws F.RelativePathError F.parse_rpath("./") 26 | @test F.parse_rpath("./a") == "/assets/pg1/a" 27 | @test F.parse_rpath("./a/b") == "/assets/pg1/a/b" 28 | @test F.parse_rpath("./a/b", canonical=true) == joinpath(F.PATHS[:site], "assets", "pg1", "a", "b") 29 | 30 | # [path] 31 | @test_throws F.RelativePathError F.parse_rpath("") 32 | @test F.parse_rpath("blah") == "/assets/blah" 33 | @test F.parse_rpath("blah", code=true) == "/assets/pg1/code/blah" 34 | @test F.parse_rpath("blah", canonical=true) == joinpath(F.PATHS[:site], "assets", "blah") 35 | end 36 | 37 | @testset "resolve_rpath" begin 38 | ass = F.PATHS[:assets] 39 | write(joinpath(ass, "p1.jl"), "a = 5") 40 | write(joinpath(ass, "p2.png"), "gibberish") 41 | assout = joinpath(F.PATHS[:site], "assets") 42 | isdir(assout) && rm(assout, recursive=true) 43 | mkpath(assout) 44 | cp(joinpath(ass, "p1.jl"), joinpath(assout, "p1.jl"), force=true) 45 | cp(joinpath(ass, "p1.jl"), joinpath(assout, "p2.png"), force=true) 46 | 47 | fp, d, fn = F.resolve_rpath("/assets/p1", "julia") 48 | @test fp == joinpath(assout, "p1.jl") 49 | @test d == assout 50 | @test fn == "p1" 51 | 52 | fp, d, fn = F.resolve_rpath("/assets/p2") 53 | @test fp == joinpath(assout, "p2.png") 54 | @test d == assout 55 | @test fn == "p2" 56 | 57 | @test_throws F.FileNotFoundError F.resolve_rpath("/assets/foo") 58 | @test_throws F.FileNotFoundError F.resolve_rpath("foo") 59 | @test_throws F.FileNotFoundError F.resolve_rpath("foo", "julia") 60 | end 61 | 62 | @testset "form_cpaths" begin 63 | ass = F.PATHS[:assets] 64 | assout = joinpath(F.PATHS[:site], "assets") 65 | isdir(assout) && rm(assout, recursive=true) 66 | mkpath(assout) 67 | 68 | curpath = set_curpath(joinpath("pages", "pg1.md")) 69 | 70 | write(joinpath(ass, "p1.jl"), "a = 5") 71 | write(joinpath(ass, "p2.png"), "gibberish") 72 | 73 | sp, sd, sn, od, op, rp = F.form_codepaths("p1") 74 | @test sp == joinpath(assout, splitext(curpath)[1], "code", "p1.jl") 75 | @test sd == joinpath(assout, splitext(curpath)[1], "code") 76 | @test sn == "p1.jl" 77 | @test od == joinpath(assout, splitext(curpath)[1], "code", "output") 78 | @test op == joinpath(od, "p1.out") 79 | @test rp == joinpath(od, "p1.res") 80 | end 81 | -------------------------------------------------------------------------------- /test/coverage/extras1.jl: -------------------------------------------------------------------------------- 1 | @testset "Conv-lx" begin 2 | cd(td) 3 | # Exception instead of ArgumentError as may fail with system error 4 | @test_throws Exception F.check_input_rpath("aldjfk") 5 | end 6 | 7 | @testset "Conv-html" begin 8 | @test_throws F.HTMLFunctionError F.convert_html("{{insert bb cc}}") 9 | @test_throws F.HTMLFunctionError F.convert_html("{{href aa}}") 10 | @test (@test_logs (:warn, "Unknown dictionary name aa in {{href ...}}. Ignoring") F.convert_html("{{href aa bb}}")) == "??" 11 | @test_throws F.HTMLBlockError F.convert_html("{{if asdf}}{{end}}") 12 | @test_throws F.HTMLBlockError F.convert_html("{{if asdf}}") 13 | @test_throws F.HTMLBlockError F.convert_html("{{isdef asdf}}") 14 | @test_throws F.HTMLBlockError F.convert_html("{{ispage asdf}}") 15 | end 16 | 17 | @testset "Conv-md" begin 18 | s = """ 19 | @def blah 20 | """ 21 | @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)) == "" 22 | 23 | s = """ 24 | Blah 25 | [^1]: hello 26 | """ |> fd2html_td 27 | @test isapproxstr(s, "

Blah

") 28 | end 29 | 30 | @testset "Franklin" begin 31 | cd(td); mkpath("foo"); cd("foo"); write("config.md","") 32 | @test_throws ArgumentError serve(single=true) 33 | cd(td) 34 | end 35 | 36 | @testset "RSS" begin 37 | F.set_var!(F.GLOBAL_VARS, "website_descr", "") 38 | F.RSS_DICT["hello"] = F.RSSItem("","","","","","","",Date(1)) 39 | @test (@test_logs (:warn, """ 40 | I found RSS items but the RSS feed is not properly described: 41 | at least one of the following variables has not been defined in 42 | your config.md: `website_title`, `website_descr`, `website_url`. 43 | The feed will not be (re)generated.""") F.rss_generator()) === nothing 44 | end 45 | 46 | 47 | @testset "parser-lx" begin 48 | s = raw""" 49 | \newcommand{hello}{hello} 50 | """ 51 | @test_throws F.LxDefError (s |> fd2html) 52 | s = raw""" 53 | \foo 54 | """ 55 | @test_throws F.LxComError (s |> fd2html) 56 | s = raw""" 57 | \newcommand{\foo}[2]{hello #1 #2} 58 | \foo{a} {} 59 | """ 60 | @test_throws F.LxComError (s |> fd2html) 61 | end 62 | 63 | @testset "ocblocks" begin 64 | s = raw""" 65 | @@foo 66 | """ 67 | @test_throws F.OCBlockError (s |> fd2html) 68 | end 69 | 70 | @testset "tofrom" begin 71 | s = "jμΛΙα" 72 | @test F.from(s) == 1 73 | @test F.to(s) == lastindex(s) 74 | end 75 | 76 | 77 | @testset "{{toc}}" begin 78 | s = """ 79 | ~~~ 80 | {{toc 1 2}} 81 | ~~~ 82 | # Hello 83 | ## Goodbye 84 | """ |> fd2html_td 85 | @test isapproxstr(s, raw""" 86 | 87 |

Hello

Goodbye

""") 88 | end 89 | 90 | 91 | @testset "franklin" begin 92 | fs1() 93 | gotd() 94 | write(joinpath(td, "config.md"), "foo") 95 | @test_throws ArgumentError serve(single=true) 96 | end 97 | -------------------------------------------------------------------------------- /src/eval/run.jl: -------------------------------------------------------------------------------- 1 | #= 2 | Functionalities to take a string corresponding to Julia code and evaluate 3 | that code in a given module while capturing stdout and redirecting it to 4 | a file. 5 | =# 6 | 7 | """ 8 | $SIGNATURES 9 | 10 | Consumes a string with Julia code, returns a vector of expression(s). 11 | 12 | Note: this function was adapted from the `parse_input` function from Weave.jl. 13 | """ 14 | function parse_code(code::AS) 15 | exs = Any[] # Expr, Symbol or Any Julia core value 16 | n = sizeof(code) 17 | pos = 1 18 | while pos ≤ n 19 | ex, pos = Meta.parse(code, pos) 20 | isnothing(ex) && continue 21 | push!(exs, ex) 22 | end 23 | exs 24 | end 25 | 26 | """ 27 | $SIGNATURES 28 | 29 | Run some code in a given module while redirecting stdout to a given path. 30 | Return the result of the evaluation or `nothing` if the code was empty or 31 | the evaluation failed. 32 | If the evaluation errors, the error is printed to output then a warning is 33 | shown. 34 | 35 | ## Arguments 36 | 37 | 1. `mod`: the module in which to evaluate the code, 38 | 1. `code`: string corresponding to the code, 39 | 1. `out_path`: path where stdout should be redirected 40 | 41 | ## Keywords 42 | 43 | * `warn_err=true`: whether to show a warning in the REPL if there was an error 44 | running the code. 45 | * `strip=false`: whether to strip the code, this may already have been done. 46 | """ 47 | function run_code(mod::Module, code::AS, out_path::AS; 48 | warn_err::Bool=true, strip_code::Bool=true) 49 | isempty(code) && return nothing 50 | strip_code && (code = strip(code)) 51 | exs = parse_code(strip(code)) 52 | ne = length(exs) 53 | res = nothing # to capture final result 54 | err = nothing 55 | ispath(out_path) || mkpath(dirname(out_path)) 56 | open(out_path, "w") do outf 57 | if !FD_ENV[:SILENT_MODE] 58 | rprint("→ evaluating code [...] ($(locvar("fd_rpath")))") 59 | end 60 | redirect_stdout(outf) do 61 | e = 1 62 | while e <= ne 63 | try 64 | res = Core.eval(mod, exs[e]) 65 | catch e 66 | io = IOBuffer() 67 | showerror(io, e) 68 | println(String(take!(io))) 69 | err = typeof(e) 70 | break 71 | end 72 | e += 1 73 | end 74 | end 75 | end 76 | # if there was an error, return nothing and possibly show warning 77 | if !isnothing(err) 78 | # TODO: add more informative message, maybe show type of error 79 | # + parent path 80 | FD_ENV[:SILENT_MODE] || print("\n") 81 | warn_err && @warn "There was an error of type $err running the code." 82 | res = nothing 83 | end 84 | # if last bit ends with `;` return nothing (no display) 85 | code[end] == ';' && return nothing 86 | # if last line is a Julia value return 87 | isa(exs[end], Expr) || return res 88 | # if last line of the code is a `show` 89 | if length(exs[end].args) > 1 && exs[end].args[1] == Symbol("@show") 90 | return nothing 91 | end 92 | # otherwise return the result of the last expression 93 | return res 94 | end 95 | -------------------------------------------------------------------------------- /test/manager/rss.jl: -------------------------------------------------------------------------------- 1 | @testset "RSSItem" begin 2 | rss = F.RSSItem( 3 | "title", "www.link.com", "description", "author@author.com", "category", 4 | "www.comments.com", "enclosure", Date(2012,12,12)) 5 | @test rss.title == "title" 6 | @test rss.link == "www.link.com" 7 | @test rss.description == "description" 8 | @test rss.author == "author@author.com" 9 | @test rss.category == "category" 10 | @test rss.comments == "www.comments.com" 11 | @test rss.enclosure == "enclosure" 12 | @test rss.pubDate == Date(2012,12,12) 13 | end 14 | 15 | @testset "RSSbasics" begin 16 | fs1() 17 | empty!(F.RSS_DICT) 18 | F.def_GLOBAL_VARS!() 19 | F.set_var!(F.GLOBAL_VARS, "website_title", "Website title") 20 | F.set_var!(F.GLOBAL_VARS, "website_descr", "Website descr") 21 | F.set_var!(F.GLOBAL_VARS, "website_url", "https://github.com/tlienart/Franklin.jl/") 22 | F.def_LOCAL_VARS!() 23 | set_curpath("hey/ho.md") 24 | F.set_var!(F.LOCAL_VARS, "rss_title", "title") 25 | F.set_var!(F.LOCAL_VARS, "rss", "A **description** done.") 26 | F.set_var!(F.LOCAL_VARS, "rss_author", "chuck@norris.com") 27 | 28 | item = F.add_rss_item() 29 | @test item.title == "title" 30 | @test item.description == "A description done.\n" 31 | @test item.author == "chuck@norris.com" 32 | # unchanged bc all three fallbacks lead to Data(1) 33 | @test item.pubDate == Date(1) 34 | 35 | F.set_var!(F.LOCAL_VARS, "rss_title", "") 36 | @test @test_logs (:warn, "Found an RSS description but no title for page /hey/ho.html.") F.add_rss_item().title == "" 37 | 38 | @test F.RSS_DICT["/hey/ho.html"].description == item.description 39 | 40 | # Generation 41 | F.PATHS[:folder] = td 42 | F.rss_generator() 43 | feed = joinpath(F.PATHS[:folder], "feed.xml") 44 | @test isfile(feed) 45 | fc = prod(readlines(feed, keep=true)) 46 | @test occursin("description done.", fc) 47 | end 48 | 49 | @testset "RSSbasics" begin 50 | fs2() 51 | empty!(F.RSS_DICT) 52 | F.def_GLOBAL_VARS!() 53 | F.set_var!(F.GLOBAL_VARS, "website_title", "Website title") 54 | F.set_var!(F.GLOBAL_VARS, "website_descr", "Website descr") 55 | F.set_var!(F.GLOBAL_VARS, "website_url", "https://github.com/tlienart/Franklin.jl/") 56 | F.def_LOCAL_VARS!() 57 | set_curpath("hey/ho.md") 58 | F.set_var!(F.LOCAL_VARS, "rss_title", "title") 59 | F.set_var!(F.LOCAL_VARS, "rss", "A **description** done.") 60 | F.set_var!(F.LOCAL_VARS, "rss_author", "chuck@norris.com") 61 | 62 | item = F.add_rss_item() 63 | @test item.title == "title" 64 | @test item.description == "A description done.\n" 65 | @test item.author == "chuck@norris.com" 66 | # unchanged bc all three fallbacks lead to Data(1) 67 | @test item.pubDate == Date(1) 68 | 69 | F.set_var!(F.LOCAL_VARS, "rss_title", "") 70 | @test @test_logs (:warn, "Found an RSS description but no title for page /hey/ho/index.html.") F.add_rss_item().title == "" 71 | 72 | @test F.RSS_DICT["/hey/ho/index.html"].description == item.description 73 | 74 | # Generation 75 | F.PATHS[:folder] = td 76 | F.rss_generator() 77 | feed = joinpath(F.PATHS[:site], "feed.xml") 78 | @test isfile(feed) 79 | fc = prod(readlines(feed, keep=true)) 80 | @test occursin("description done.", fc) 81 | end 82 | -------------------------------------------------------------------------------- /test/global/ordering.jl: -------------------------------------------------------------------------------- 1 | # Following multiple issues over time with ordering, this attempts to have 2 | # bunch of test cases where it's clear in what order things are tokenized / found 3 | 4 | @testset "Ordering-1" begin 5 | st = raw""" 6 | A 7 | 11 | B 12 | """ 13 | steps = st |> explore_md_steps 14 | blocks, = steps[:ocblocks] 15 | @test length(blocks) == 1 16 | @test blocks[1].name == :COMMENT 17 | @test isapproxstr(st |> seval, """ 18 |

A

19 |

B

20 | """) 21 | end 22 | 23 | @testset "Ordering-2" begin 24 | st = raw""" 25 | A 26 | \begin{eqnarray} 27 | 1 + 1 &=& 2 28 | \end{eqnarray} 29 | B 30 | """ 31 | steps = st |> explore_md_steps 32 | blocks, = steps[:ocblocks] 33 | @test length(blocks) == 1 34 | @test blocks[1].name == :MATH_EQA 35 | 36 | @test isapproxstr(st |> seval, raw""" 37 |

A 38 | \[\begin{array}{c} 39 | 1 + 1 &=& 2 40 | \end{array}\] 41 | B

""") 42 | end 43 | 44 | @testset "Ordering-3" begin 45 | st = raw""" 46 | A 47 | \begin{eqnarray} 48 | 1 + 1 &=& 2 49 | \end{eqnarray} 50 | B 51 | 58 | C 59 | """ 60 | steps = st |> explore_md_steps 61 | blocks, = steps[:ocblocks] 62 | @test length(blocks) == 2 63 | @test blocks[1].name == :MATH_EQA 64 | @test blocks[2].name == :COMMENT 65 | 66 | @test isapproxstr(st |> seval, raw""" 67 |

A 68 | \[\begin{array}{c} 69 | 1 + 1 &=& 2 70 | \end{array}\] 71 | B

72 |

C

""") 73 | end 74 | 75 | @testset "Ordering-4" begin 76 | st = raw""" 77 | \newcommand{\eqa}[1]{\begin{eqnarray}#1\end{eqnarray}} 78 | A 79 | \eqa{ 80 | B 81 | } 82 | C 83 | """ 84 | steps = st |> explore_md_steps 85 | blocks, = steps[:ocblocks] 86 | 87 | @test length(blocks) == 3 88 | @test all(getproperty.(blocks, :name) .== :LXB) 89 | 90 | @test isapproxstr(st |> seval, raw""" 91 |

A 92 | \[\begin{array}{c} 93 | B 94 | \end{array}\] 95 | C

""") 96 | end 97 | 98 | @testset "Ordering-5" begin 99 | st = raw""" 100 | A [❗️_ongoing_ ] C 101 | """ 102 | @test isapproxstr(st |> seval, raw""" 103 |

A [❗️ongoing ] C

104 | """) 105 | st = raw""" 106 | 0 107 | * A 108 | * B [❗️_ongoing_ ] 112 | C 113 | """ 114 | @test isapproxstr(st |> seval, raw""" 115 |

0

116 |
    117 |
  • A

    118 |
      119 |
    • B [❗️ongoing ]

    • 120 |
    121 |
  • 122 |
123 |

C

124 | """) 125 | end 126 | -------------------------------------------------------------------------------- /src/utils/paths.jl: -------------------------------------------------------------------------------- 1 | """ 2 | FOLDER_PATH 3 | 4 | Container to keep track of where Franklin is being run. 5 | """ 6 | const FOLDER_PATH = Ref{String}() 7 | 8 | 9 | """ 10 | IGNORE_FILES 11 | 12 | Collection of file names that will be ignored at compile time. 13 | """ 14 | const IGNORE_FILES = [".DS_Store", ".gitignore", "LICENSE.md", "README.md", 15 | "franklin", "franklin.pub", "node_modules/"] 16 | 17 | 18 | """ 19 | INFRA_EXT 20 | 21 | Collection of file extensions considered as potential infrastructure files. 22 | """ 23 | const INFRA_FILES = [".html", ".css"] 24 | 25 | 26 | """ 27 | PATHS 28 | 29 | Dictionary for the paths of the input folders and the output folders. The simpler case only 30 | requires the main input folder to be defined i.e. `PATHS[:src]` and infers the others via the 31 | `set_paths!()` function. 32 | """ 33 | const PATHS = LittleDict{Symbol,String}() 34 | 35 | 36 | """ 37 | $(SIGNATURES) 38 | 39 | This assigns all the paths where files will be read and written with root the `FOLDER_PATH` 40 | which is assigned at runtime. 41 | """ 42 | function set_paths!()::LittleDict{Symbol,String} 43 | @assert isassigned(FOLDER_PATH) "FOLDER_PATH undefined" 44 | @assert isdir(FOLDER_PATH[]) "FOLDER_PATH is not a valid path" 45 | 46 | # NOTE it is recommended not to change the names of those paths. 47 | # Particularly for the output dir. If you do, check for example that 48 | # functions such as Franklin.publish point to the right dirs/files. 49 | 50 | if FD_ENV[:STRUCTURE] < v"0.2" 51 | PATHS[:folder] = normpath(FOLDER_PATH[]) 52 | PATHS[:src] = joinpath(PATHS[:folder], "src") 53 | PATHS[:src_pages] = joinpath(PATHS[:src], "pages") 54 | PATHS[:src_css] = joinpath(PATHS[:src], "_css") 55 | PATHS[:src_html] = joinpath(PATHS[:src], "_html_parts") 56 | PATHS[:pub] = joinpath(PATHS[:folder], "pub") 57 | PATHS[:css] = joinpath(PATHS[:folder], "css") 58 | PATHS[:libs] = joinpath(PATHS[:folder], "libs") 59 | PATHS[:assets] = joinpath(PATHS[:folder], "assets") 60 | PATHS[:literate] = joinpath(PATHS[:folder], "scripts") 61 | PATHS[:tag] = joinpath(PATHS[:pub], "tag") 62 | else 63 | PATHS[:folder] = normpath(FOLDER_PATH[]) 64 | PATHS[:site] = joinpath(PATHS[:folder], "__site") # mandatory 65 | PATHS[:assets] = joinpath(PATHS[:folder], "_assets") # mandatory 66 | PATHS[:css] = joinpath(PATHS[:folder], "_css") # mandatory 67 | PATHS[:layout] = joinpath(PATHS[:folder], "_layout") # mandatory 68 | PATHS[:libs] = joinpath(PATHS[:folder], "_libs") # mandatory 69 | PATHS[:literate] = joinpath(PATHS[:folder], "_literate") # optional 70 | PATHS[:tag] = joinpath(PATHS[:site], "tag") 71 | end 72 | 73 | return PATHS 74 | end 75 | 76 | """ 77 | path(s) 78 | 79 | Return the paths corresponding to `s` e.g. `path(:folder)`. 80 | """ 81 | path(s) = PATHS[Symbol(s)] 82 | 83 | 84 | """ 85 | Pointer to the `/output/` folder associated with an eval block (see also 86 | [`@OUTPUT`](@ref)). 87 | """ 88 | const OUT_PATH = Ref("") 89 | 90 | """ 91 | This macro points to the `/output/` folder associated with an eval block. 92 | So for instance, if an eval block generates a plot, you could save the plot 93 | with something like `savefig(joinpath(@OUTPUT, "ex1.png"))`. 94 | """ 95 | macro OUTPUT() 96 | return OUT_PATH[] 97 | end 98 | -------------------------------------------------------------------------------- /test/utils/folder_structure.jl: -------------------------------------------------------------------------------- 1 | @testset "set_paths!" begin 2 | root = F.FOLDER_PATH[] = mktempdir() 3 | empty!(F.PATHS); F.FD_ENV[:STRUCTURE] = v"0.1"; F.set_paths!() 4 | @test Set(keys(F.PATHS)) == Set([:folder, :src, :src_pages, :src_css, :src_html, :pub, :css, :libs, :assets, :literate, :tag]) 5 | @test F.PATHS[:folder] == root 6 | @test F.PATHS[:src_pages] == joinpath(F.PATHS[:src], "pages") 7 | 8 | # ================================================ 9 | empty!(F.PATHS); F.FD_ENV[:STRUCTURE] = v"0.2"; F.set_paths!() 10 | @test Set(keys(F.PATHS)) == Set([:folder, :assets, :css, :layout, :libs, :literate, :site, :tag]) 11 | @test F.PATHS[:folder] == root 12 | @test F.PATHS[:site] == joinpath(root, "__site") 13 | @test F.PATHS[:assets] == joinpath(root, "_assets") 14 | @test F.PATHS[:css] == joinpath(root, "_css") 15 | @test F.PATHS[:layout] == joinpath(root, "_layout") 16 | @test F.PATHS[:libs] == joinpath(root, "_libs") 17 | @test F.PATHS[:literate] == joinpath(root, "_literate") 18 | @test F.PATHS[:tag] == joinpath(root, "__site", "tag") 19 | 20 | # ================================================ 21 | # reset the structure to legacy for further tests 22 | empty!(F.PATHS); F.FD_ENV[:STRUCTURE] = v"0.1"; F.set_paths!() 23 | end 24 | 25 | @testset "outp_path" begin 26 | empty!(F.PATHS); F.FD_ENV[:STRUCTURE] = v"0.1"; F.set_paths!() 27 | # MD_PAGES 28 | out = F.form_output_path(F.PATHS[:src], "index.md", :md) 29 | @test out == joinpath(F.PATHS[:folder], "index.html") 30 | 31 | out = F.form_output_path(F.PATHS[:src_pages], "foo.md", :md) 32 | @test out == joinpath(F.PATHS[:folder], "pub", "foo.html") 33 | 34 | # CSS 35 | out = F.form_output_path(F.PATHS[:src_css], "foo.css", :infra) 36 | @test out == joinpath(F.PATHS[:css], "foo.css") 37 | 38 | # (note: assets, libs are not tracked and not considered in v1) 39 | 40 | # ================================================ 41 | 42 | empty!(F.PATHS); F.FD_ENV[:STRUCTURE] = v"0.2"; F.set_paths!() 43 | 44 | # MD_PAGES 45 | base = F.PATHS[:folder] 46 | file = "index.md" 47 | out = F.form_output_path(base, file, :md) 48 | @test out == joinpath(F.PATHS[:site], "index.html") 49 | 50 | file = "index.html" 51 | out = F.form_output_path(base, file, :html) 52 | @test out == joinpath(F.PATHS[:site], "index.html") 53 | 54 | file = "page.md" 55 | out = F.form_output_path(base, file, :md) 56 | @test out == joinpath(F.PATHS[:site], "page", "index.html") 57 | 58 | file = "page.html" 59 | out = F.form_output_path(base, file, :html) 60 | @test out == joinpath(F.PATHS[:site], "page", "index.html") 61 | 62 | file = "index.md" 63 | base = joinpath(F.PATHS[:folder], "menu") 64 | out = F.form_output_path(base, file, :md) 65 | @test out == joinpath(F.PATHS[:site], "menu", "index.html") 66 | 67 | file = "index.html" 68 | out = F.form_output_path(base, file, :html) 69 | @test out == joinpath(F.PATHS[:site], "menu", "index.html") 70 | 71 | file = "page.md" 72 | out = F.form_output_path(base, file, :md) 73 | @test out == joinpath(F.PATHS[:site], "menu", "page", "index.html") 74 | 75 | # OTHER STUFF 76 | file = "foo.css" 77 | base = F.PATHS[:css] 78 | out = F.form_output_path(base, file, :infra) 79 | @test out == joinpath(F.PATHS[:site], "css", "foo.css") 80 | file = "lib.js" 81 | base = F.PATHS[:libs] 82 | out = F.form_output_path(base, file, :infra) 83 | @test out == joinpath(F.PATHS[:site], "libs", "lib.js") 84 | 85 | # ================================================ 86 | 87 | empty!(F.PATHS); F.FD_ENV[:STRUCTURE] = v"0.1"; F.set_paths!() 88 | end 89 | -------------------------------------------------------------------------------- /test/integration/literate.jl: -------------------------------------------------------------------------------- 1 | fs2() 2 | 3 | mkpath(F.path(:literate)) 4 | 5 | @testset "Literate-0" begin 6 | @test_throws ErrorException literate_folder("foo/") 7 | litpath = literate_folder("_literate/") 8 | @test litpath == literate_folder(F.path(:literate)) 9 | end 10 | 11 | @testset "Literate-a" begin 12 | # Post processing: numbering of julia blocks 13 | s = raw""" 14 | A 15 | 16 | ```julia 17 | B 18 | ``` 19 | 20 | C 21 | 22 | ```julia 23 | D 24 | ``` 25 | """ 26 | @test F.literate_post_process(s) == """ 27 | 28 | A 29 | 30 | ```julia:ex1 31 | B 32 | ``` 33 | 34 | C 35 | 36 | ```julia:ex2 37 | D 38 | ``` 39 | """ 40 | end 41 | 42 | @testset "Literate-b" begin 43 | # Literate to Franklin 44 | s = raw""" 45 | # # Rational numbers 46 | # 47 | # In julia rational numbers can be constructed with the `//` operator. 48 | # Lets define two rational numbers, `x` and `y`: 49 | 50 | ## Define variable x and y 51 | x = 1//3 52 | y = 2//5 53 | 54 | # When adding `x` and `y` together we obtain a new rational number: 55 | 56 | z = x + y 57 | """ 58 | path = joinpath(F.path(:literate), "tutorial.jl") 59 | write(path, s) 60 | opath, = F.literate_to_franklin("/_literate/tutorial") 61 | @test endswith(opath, joinpath(F.PATHS[:site], "assets", "literate", "tutorial.md")) 62 | out = read(opath, String) 63 | @test out == """ 64 | 65 | # Rational numbers 66 | 67 | In julia rational numbers can be constructed with the `//` operator. 68 | Lets define two rational numbers, `x` and `y`: 69 | 70 | ```julia:ex1 71 | # Define variable x and y 72 | x = 1//3 73 | y = 2//5 74 | ``` 75 | 76 | When adding `x` and `y` together we obtain a new rational number: 77 | 78 | ```julia:ex2 79 | z = x + y 80 | ``` 81 | 82 | """ 83 | 84 | # Use of `\literate` command 85 | h = raw""" 86 | @def hascode = true 87 | @def showall = true 88 | @def reeval = true 89 | 90 | \literate{/_literate/tutorial.jl} 91 | """ |> fd2html_td 92 | @test isapproxstr(h, """ 93 |

Rational numbers

94 |

In julia rational numbers can be constructed with the // operator. Lets define two rational numbers, x and y:

95 |
# Define variable x and y
 96 |         x = 1//3
 97 |         y = 2//5
98 |
2//5
99 |

When adding x and y together we obtain a new rational number:

100 |
z = x + y
101 |
11//15
102 | """) 103 | end 104 | 105 | @testset "Literate-c" begin 106 | s = raw""" 107 | \literate{foo} 108 | """ 109 | @test_throws F.LiterateRelativePathError (s |> fd2html_td) 110 | s = raw""" 111 | \literate{/foo} 112 | """ 113 | @test @test_logs (:warn, "File not found when trying to convert a literate file ($(joinpath(F.PATHS[:folder], "foo.jl"))).") (s |> fd2html_td) == """

// Literate file matching '/foo' not found. //

\n""" 114 | end 115 | -------------------------------------------------------------------------------- /test/integration/literate_fs2.jl: -------------------------------------------------------------------------------- 1 | fs1() 2 | 3 | scripts = joinpath(F.PATHS[:folder], "literate-scripts") 4 | mkpath(scripts) 5 | 6 | @testset "Literate-0" begin 7 | @test_throws ErrorException literate_folder("foo/") 8 | litpath = literate_folder("literate-scripts/") 9 | @test litpath == joinpath(F.PATHS[:folder], "literate-scripts/") 10 | end 11 | 12 | @testset "Literate-a" begin 13 | # Post processing: numbering of julia blocks 14 | s = raw""" 15 | A 16 | 17 | ```julia 18 | B 19 | ``` 20 | 21 | C 22 | 23 | ```julia 24 | D 25 | ``` 26 | """ 27 | @test F.literate_post_process(s) == """ 28 | 29 | A 30 | 31 | ```julia:ex1 32 | B 33 | ``` 34 | 35 | C 36 | 37 | ```julia:ex2 38 | D 39 | ``` 40 | """ 41 | end 42 | 43 | @testset "Literate-b" begin 44 | # Literate to Franklin 45 | s = raw""" 46 | # # Rational numbers 47 | # 48 | # In julia rational numbers can be constructed with the `//` operator. 49 | # Lets define two rational numbers, `x` and `y`: 50 | 51 | ## Define variable x and y 52 | x = 1//3 53 | y = 2//5 54 | 55 | # When adding `x` and `y` together we obtain a new rational number: 56 | 57 | z = x + y 58 | """ 59 | path = joinpath(scripts, "tutorial.jl") 60 | write(path, s) 61 | opath, = F.literate_to_franklin("/literate-scripts/tutorial") 62 | @test endswith(opath, joinpath(F.PATHS[:assets], "literate", "tutorial.md")) 63 | out = read(opath, String) 64 | @test out == """ 65 | 66 | # Rational numbers 67 | 68 | In julia rational numbers can be constructed with the `//` operator. 69 | Lets define two rational numbers, `x` and `y`: 70 | 71 | ```julia:ex1 72 | # Define variable x and y 73 | x = 1//3 74 | y = 2//5 75 | ``` 76 | 77 | When adding `x` and `y` together we obtain a new rational number: 78 | 79 | ```julia:ex2 80 | z = x + y 81 | ``` 82 | 83 | """ 84 | 85 | # Use of `\literate` command 86 | h = raw""" 87 | @def hascode = true 88 | @def showall = true 89 | @def reeval = true 90 | 91 | \literate{/literate-scripts/tutorial.jl} 92 | """ |> fd2html_td 93 | @test isapproxstr(h, """ 94 |

Rational numbers

95 |

In julia rational numbers can be constructed with the // operator. Lets define two rational numbers, x and y:

96 |
# Define variable x and y
 97 |         x = 1//3
 98 |         y = 2//5
99 |
2//5
100 |

When adding x and y together we obtain a new rational number:

101 |
z = x + y
102 |
11//15
103 | """) 104 | end 105 | 106 | @testset "Literate-c" begin 107 | s = raw""" 108 | \literate{foo} 109 | """ 110 | @test_throws F.LiterateRelativePathError (s |> fd2html_td) 111 | s = raw""" 112 | \literate{/foo} 113 | """ 114 | @test @test_logs (:warn, "File not found when trying to convert a literate file ($(joinpath(F.PATHS[:folder], "foo.jl"))).") (s |> fd2html_td) == """

// Literate file matching '/foo' not found. //

\n""" 115 | end 116 | -------------------------------------------------------------------------------- /test/converter/md/hyperref.jl: -------------------------------------------------------------------------------- 1 | @testset "Hyperref" begin 2 | st = raw""" 3 | Some string 4 | $$ x = x \label{eq 1}$$ 5 | then as per \citet{amari98b} also this \citep{bardenet17} and 6 | \cite{amari98b, bardenet17} 7 | Reference to equation: \eqref{eq 1}. 8 | 9 | Then maybe some text etc. 10 | 11 | * \biblabel{amari98b}{Amari and Douglas., 1998} **Amari** and **Douglas**: *Why Natural Gradient*, 1998. 12 | * \biblabel{bardenet17}{Bardenet et al., 2017} **Bardenet**, **Doucet** and **Holmes**: *On Markov Chain Monte Carlo Methods for Tall Data*, 2017. 13 | """; 14 | 15 | F.def_GLOBAL_VARS!() 16 | F.def_GLOBAL_LXDEFS!() 17 | 18 | m = F.convert_md(st) 19 | 20 | h1 = F.refstring("eq 1") 21 | h2 = F.refstring("amari98b") 22 | h3 = F.refstring("bardenet17") 23 | 24 | @test haskey(F.PAGE_EQREFS, h1) 25 | @test haskey(F.PAGE_BIBREFS, h2) 26 | @test haskey(F.PAGE_BIBREFS, h3) 27 | 28 | @test F.PAGE_EQREFS[h1] == 1 # first equation 29 | @test F.PAGE_BIBREFS[h2] == "Amari and Douglas., 1998" 30 | @test F.PAGE_BIBREFS[h3] == "Bardenet et al., 2017" 31 | 32 | h = F.convert_html(m) 33 | 34 | @test isapproxstr(h, """ 35 |

36 | Some string 37 | \\[ x = x \\] 38 | then as per Amari and Douglas., 1998 also this (Bardenet et al., 2017) and 39 | Amari and Douglas., 1998, Bardenet et al., 2017 40 | Reference to equation: (1) . 41 |

42 |

43 | Then maybe some text etc. 44 |

45 |
    46 |
  • Amari and Douglas: Why Natural Gradient, 1998.

  • 47 |
  • Bardenet, Doucet and Holmes: On Markov Chain Monte Carlo Methods for Tall Data, 2017.

  • 48 |
49 | """) 50 | end 51 | 52 | @testset "Href-space" begin 53 | st = raw""" 54 | A 55 | $$ x = x \label{eq 1}$$ 56 | B 57 | C \eqref{eq 1}. 58 | and *B $E$*. 59 | """ 60 | h = st |> seval 61 | @test occursin(raw"""\[ x = x \]""", h) 62 | @test occursin(raw"""(1).""", h) 63 | @test occursin(raw"""B \(E\).""", h) 64 | end 65 | 66 | @testset "Eqref" begin 67 | st = raw""" 68 | \newcommand{\E}[1]{\mathbb E\left[#1\right]} 69 | \newcommand{\eqa}[1]{\begin{eqnarray}#1\end{eqnarray}} 70 | \newcommand{\R}{\mathbb R} 71 | Then something like 72 | \eqa{ \E{f(X)} \in \R &\text{if}& f:\R\maptso\R} 73 | and then 74 | \eqa{ 1+1 &=& 2 \label{eq:a trivial one}} 75 | but further 76 | \eqa{ 1 &=& 1 \label{beyond hope}} 77 | and finally a \eqref{eq:a trivial one} and maybe \eqref{beyond hope}. 78 | """ 79 | m = F.convert_md(st, collect(values(F.GLOBAL_LXDEFS))) 80 | 81 | @test F.PAGE_EQREFS[F.PAGE_EQREFS_COUNTER] == 3 82 | @test F.PAGE_EQREFS[F.refstring("eq:a trivial one")] == 2 83 | @test F.PAGE_EQREFS[F.refstring("beyond hope")] == 3 84 | 85 | h1 = F.refstring("eq:a trivial one") 86 | h2 = F.refstring("beyond hope") 87 | 88 | m == "

Then something like \$\$\\begin{array}{c} \\mathbb E\\left[ f(X)\\right] \\in \\mathbb R &\\text{if}& f:\\mathbb R\\maptso\\mathbb R\\end{array}\$\$ and then \$\$\\begin{array}{c} 1+1 &=&2 \\end{array}\$\$ but further \$\$\\begin{array}{c} 1 &=& 1 \\end{array}\$\$ and finally a ({{href EQR $h1}}) and maybe ({{href EQR $h2}}).

\n" 89 | end 90 | -------------------------------------------------------------------------------- /test/converter/md/tags.jl: -------------------------------------------------------------------------------- 1 | @testset "tagpages" begin 2 | fs2() 3 | write(joinpath("_layout", "head.html"), "") 4 | write(joinpath("_layout", "foot.html"), "") 5 | write(joinpath("_layout", "page_foot.html"), "") 6 | write("config.md", "") 7 | write("index.md", """ 8 | Hello 9 | """) 10 | F.def_GLOBAL_VARS!() 11 | F.def_LOCAL_VARS!() 12 | write("pg1.md", "@def title = \"Page1\"") 13 | write("pg2.md", "@def title = \"Page2\"") 14 | F.set_var!(F.GLOBAL_VARS, "fd_page_tags", F.DTAG(("pg1" => Set(["aa", "bb"]),))) 15 | F.globvar("fd_page_tags")["pg2"] = Set(["bb", "cc"]) 16 | 17 | @test Set(keys(F.globvar("fd_page_tags"))) == Set(["pg1", "pg2"]) 18 | @test Set(union(values(F.globvar("fd_page_tags"))...)) == Set(["aa", "bb", "cc"]) 19 | 20 | F.generate_tag_pages() 21 | 22 | @test F.globvar("fd_tag_pages")["aa"] == ["pg1"] 23 | @test F.globvar("fd_tag_pages")["bb"] == ["pg1","pg2"] 24 | @test F.globvar("fd_tag_pages")["cc"] == ["pg2"] 25 | 26 | @test isdir(F.path(:tag)) 27 | @test isfile(joinpath(F.path(:tag), "aa", "index.html")) 28 | @test isfile(joinpath(F.path(:tag), "bb", "index.html")) 29 | @test isfile(joinpath(F.path(:tag), "cc", "index.html")) 30 | 31 | tagbb = read(joinpath(F.path(:tag), "bb", "index.html"), String) 32 | tagcc = read(joinpath(F.path(:tag), "cc", "index.html"), String) 33 | 34 | @test occursin("""""", tagbb) 35 | @test occursin("""""", tagcc) 36 | 37 | F.clear_dicts() 38 | end 39 | 40 | # ======= INTEGRATION ============ 41 | 42 | @testset "tags" begin 43 | fs2() 44 | write(joinpath("_layout", "head.html"), "") 45 | write(joinpath("_layout", "foot.html"), "") 46 | write(joinpath("_layout", "page_foot.html"), "") 47 | write("config.md", "") 48 | write("index.md", """ 49 | Hello 50 | """) 51 | F.def_GLOBAL_VARS!() 52 | F.def_LOCAL_VARS!() 53 | isdir("blog") && rm("blog", recursive=true) 54 | mkdir("blog") 55 | write(joinpath("blog", "pg1.md"), """ 56 | @def tags = ["aa", "bb", "cc"] 57 | @def date = Date(2000, 01, 01) 58 | @def title = "Page 1" 59 | """) 60 | write(joinpath("blog", "pg2.md"), """ 61 | @def tags = ["bb", "cc"] 62 | @def date = Date(2001, 01, 01) 63 | @def title = "Page 2" 64 | """) 65 | write(joinpath("blog", "pg3.md"), """ 66 | @def tags = ["bb", "cc", "ee", "dd"] 67 | @def date = Date(2002, 01, 01) 68 | @def title = "Page 3" 69 | """) 70 | write(joinpath("blog", "pg4.md"), """ 71 | @def tags = ["aa", "dd", "ee"] 72 | @def date = Date(2003, 01, 01) 73 | @def title = "Page 4" 74 | """) 75 | serve(clear=true, single=true, cleanup=false, nomess=true) 76 | @test isdir(joinpath("__site", "tag")) 77 | for tag in ("aa", "bb", "cc", "dd", "ee") 78 | local p 79 | p = joinpath("__site", "tag", tag, "index.html") 80 | @test isfile(p) 81 | end 82 | 83 | p = joinpath("__site", "tag", "aa", "index.html") 84 | c = read(p, String) 85 | @test occursin("""Tag: aa""", c) 86 | @test occursin("""
""", c) 87 | @test occursin("""""", c) 89 | 90 | # Now remove some pages; we use bash commands so that they're blocking 91 | success(`rm $(joinpath("blog", "pg4.md"))`) 92 | success(`rm $(joinpath("blog", "pg3.md"))`) 93 | 94 | serve(clear=true, single=true, nomess=true) 95 | c = read(p, String) 96 | @test occursin("""
""", c) 97 | @test occursin("""""", c) 98 | 99 | F.clear_dicts() 100 | end 101 | -------------------------------------------------------------------------------- /test/runtests.jl: -------------------------------------------------------------------------------- 1 | using Franklin, Test, Markdown, Dates, Random, Literate 2 | const F = Franklin 3 | const R = @__DIR__ 4 | const D = joinpath(dirname(dirname(pathof(Franklin))), "test", "_dummies") 5 | 6 | F.FD_ENV[:SILENT_MODE] = true 7 | # F.FD_ENV[:DEBUG_MODE] = true 8 | F.FD_ENV[:STRUCTURE] = v"0.1" # legacy, it's switched up in the tests. 9 | 10 | # UTILS 11 | println("UTILS-1") 12 | include("utils/folder_structure.jl") 13 | include("utils/paths_vars.jl"); include("test_utils.jl") 14 | # --- 15 | println("UTILS-2") 16 | include("utils/misc.jl") 17 | include("utils/errors.jl") 18 | include("utils/html.jl") 19 | include("regexes.jl") 20 | println("🍺") 21 | 22 | # MANAGER folder 23 | println("MANAGER") 24 | include("manager/utils.jl") 25 | include("manager/utils_fs2.jl") 26 | include("manager/rss.jl") 27 | include("manager/config.jl") 28 | include("manager/config_fs2.jl") 29 | include("manager/dir_utils.jl") 30 | include("manager/page_vars_html.jl") 31 | println("🍺") 32 | 33 | # PARSER folder 34 | println("PARSER/MD+LX") 35 | include("parser/1-tokenize.jl") 36 | include("parser/2-blocks.jl") 37 | include("parser/markdown+latex.jl") 38 | include("parser/markdown-extra.jl") 39 | include("parser/footnotes+links.jl") 40 | include("parser/latex++.jl") 41 | include("parser/indentation++.jl") 42 | include("parser/md-dbb.jl") 43 | println("🍺") 44 | 45 | # EVAL 46 | println("EVAL") 47 | include("eval/module.jl") 48 | include("eval/run.jl") 49 | include("eval/io.jl") 50 | include("eval/io_fs2.jl") 51 | include("eval/codeblock.jl") 52 | include("eval/eval.jl") 53 | include("eval/eval_fs2.jl") 54 | include("eval/integration.jl") 55 | include("eval/extras.jl") 56 | 57 | # CONVERTER folder 58 | println("CONVERTER/MD") 59 | include("converter/md/markdown.jl") 60 | include("converter/md/markdown2.jl") 61 | include("converter/md/markdown3.jl") 62 | include("converter/md/markdown4.jl") 63 | include("converter/md/hyperref.jl") 64 | include("converter/md/md_defs.jl") 65 | include("converter/md/tags.jl") 66 | println("🍺") 67 | println("CONVERTER/HTML") 68 | include("converter/html/html.jl") 69 | include("converter/html/html2.jl") 70 | include("converter/html/html_for.jl") 71 | println("🍺") 72 | println("CONVERTER/LX") 73 | include("converter/lx/input.jl") 74 | include("converter/lx/simple.jl") 75 | include("converter/lx/simple_fs2.jl") 76 | println("🍺") 77 | 78 | fs2() 79 | 80 | println("GLOBAL") 81 | include("global/cases1.jl") 82 | include("global/cases2.jl") 83 | include("global/ordering.jl") 84 | include("global/html_esc.jl") 85 | 86 | begin 87 | # create temp dir to do complete integration testing (has to be here in 88 | # order to locally play nice with node variables etc, otherwise it's a big 89 | # headache) 90 | p = joinpath(D, "..", "__tmp"); 91 | # after errors, this may not have been deleted properly 92 | isdir(p) && rm(p; recursive=true, force=true) 93 | # make dir, go in it, do the tests, then get completely out (otherwise 94 | # windows can't delete the folder) 95 | mkdir(p); cd(p); 96 | include("global/postprocess.jl"); 97 | include("global/rss.jl") 98 | cd(p) 99 | include("global/eval.jl") 100 | cd(joinpath(D, "..")) 101 | # clean up 102 | rm(p; recursive=true, force=true) 103 | end 104 | cd(dirname(dirname(pathof(Franklin)))) 105 | 106 | println("TEMPLATING") 107 | include("templating/for.jl") 108 | include("templating/fill.jl") 109 | 110 | println("UTILS FILE") 111 | include("utils_file/basic.jl") 112 | 113 | println("INTEGRATION") 114 | include("integration/literate.jl") 115 | include("integration/literate_fs2.jl") 116 | include("integration/literate_extras.jl") 117 | 118 | flush_td() 119 | cd(joinpath(dirname(dirname(pathof(Franklin))))) 120 | 121 | println("COVERAGE") 122 | include("coverage/extras1.jl") 123 | include("coverage/paths.jl") 124 | 125 | println("😅 😅 😅 😅") 126 | 127 | # check quickly if the IPs in IP_CHECK are still ok 128 | println("Verifying ip addresses, if online these should succeed.") 129 | for (addr, name) in F.IP_CHECK 130 | println(rpad("Ping $name:", 13), ifelse(F.check_ping(addr), "✓", "✗"), ".") 131 | end 132 | -------------------------------------------------------------------------------- /test/global/postprocess.jl: -------------------------------------------------------------------------------- 1 | @testset "Gen&Opt" begin 2 | isdir("basic") && rm("basic", recursive=true, force=true) 3 | newsite("basic") 4 | 5 | global silly_call = 0 6 | function foo_on_write(pg, vars) 7 | global silly_call 8 | silly_call += length(pg) 9 | vars["fd_rpath"] # should not error 10 | return nothing 11 | end 12 | 13 | serve(single=true, on_write=foo_on_write) 14 | 15 | @test silly_call > 0 16 | 17 | # --------------- 18 | @test all(isdir, ("_assets", "_css", "_libs", "_layout", "__site")) 19 | @test isfile(joinpath("__site", "index.html")) 20 | @test isfile(joinpath("__site", "menu1", "index.html")) 21 | @test isfile(joinpath("__site", "menu2", "index.html")) 22 | @test isfile(joinpath("__site", "menu3", "index.html")) 23 | @test isfile(joinpath("__site", "css", "basic.css")) 24 | @test isfile(joinpath("__site", "css", "franklin.css")) 25 | 26 | # --------------- 27 | if Franklin.FD_CAN_MINIFY 28 | presize1 = stat(joinpath("__site", "css", "basic.css")).size 29 | presize2 = stat(joinpath("__site", "index.html")).size 30 | optimize(prerender=false) 31 | @test stat(joinpath("__site", "css", "basic.css")).size < presize1 32 | @test stat(joinpath("__site", "index.html")).size < presize2 33 | end 34 | # --------------- 35 | # verify all links 36 | Franklin.verify_links() 37 | 38 | # --------------- 39 | # change the prepath 40 | index = read(joinpath("__site","index.html"), String) 41 | @test occursin("=\"/css/basic.css", index) 42 | @test occursin("=\"/css/franklin.css", index) 43 | @test occursin("=\"/libs/highlight/github.min.css", index) 44 | @test occursin("=\"/libs/katex/katex.min.css", index) 45 | 46 | optimize(minify=false, prerender=false, prepath="prependme") 47 | index = read(joinpath("__site","index.html"), String) 48 | @test occursin("=\"/prependme/css/basic.css", index) 49 | @test occursin("=\"/prependme/css/franklin.css", index) 50 | @test occursin("=\"/prependme/libs/highlight/github.min.css", index) 51 | @test occursin("=\"/prependme/libs/katex/katex.min.css", index) 52 | end 53 | 54 | if F.FD_CAN_PRERENDER; @testset "prerender" begin 55 | @testset "katex" begin 56 | hs = raw""" 57 | 58 | 59 | 60 |
61 |

range is \(10\sqrt{3}\)–\(20\sqrt{2}\)

62 |

Consider an invertible matrix \(M\) made of blocks \(A\), \(B\), \(C\) and \(D\) with

63 | \[ M \quad\!\! =\quad\!\! \begin{pmatrix} A & B \\ C & D \end{pmatrix} \] 64 |
65 | """ 66 | 67 | jskx = F.js_prerender_katex(hs) 68 | # conversion of the non-ascii endash (inline) 69 | @test occursin("""–""", jskx) 70 | # conversion of `\(M\)` (inline) 71 | @test occursin("""M""", jskx) 72 | # conversion of the equation (display) 73 | @test occursin("""M""", jskx) 74 | end 75 | 76 | if F.FD_CAN_HIGHLIGHT; @testset "highlight" begin 77 | hs = raw""" 78 | 79 | 80 | 81 |
82 |

Title

83 |

Blah

84 |
using Test
85 |         # Woodbury formula
86 |         b = 2
87 |         println("hello $b")
88 |         
89 |
90 | """ 91 | jshl = F.js_prerender_highlight(hs) 92 | # conversion of the code 93 | @test occursin("""
using""", jshl)
94 |         @test occursin(raw""""hello $b"""", jshl)
95 |     end; end # if can highlight
96 | end; end # if can prerender
97 | 


--------------------------------------------------------------------------------
/src/converter/markdown/mddefs.jl:
--------------------------------------------------------------------------------
  1 | """
  2 | $(SIGNATURES)
  3 | 
  4 | Convenience function to process markdown definitions `@def ...` as appropriate.
  5 | Depending on `isconfig`, will update `GLOBAL_VARS` or `LOCAL_VARS`.
  6 | 
  7 | **Arguments**
  8 | 
  9 | * `blocks`:    vector of active docs
 10 | * `isconfig`:  whether the file being processed is the config file
 11 |                 (--> global page variables)
 12 | """
 13 | function process_mddefs(blocks::Vector{OCBlock}, isconfig::Bool,
 14 |                         pagevar::Bool=false)::Nothing
 15 | 
 16 |     (:process_mddefs, "config: $isconfig, pagevar: $pagevar") |> logger
 17 | 
 18 |     # Find all markdown definitions (MD_DEF) blocks
 19 |     mddefs = filter(β -> (β.name == :MD_DEF), blocks)
 20 |     # empty container for the assignments
 21 |     assignments = Vector{Pair{String, String}}()
 22 |     # go over the blocks, and extract the assignment
 23 |     for (i, mdd) ∈ enumerate(mddefs)
 24 |         inner = stent(mdd)
 25 |         m = match(ASSIGN_PAT, inner)
 26 |         if isnothing(m)
 27 |             @warn "Found delimiters for an @def environment but it didn't " *
 28 |                   "have the right @def var = ... format. Verify (ignoring " *
 29 |                   "for now)."
 30 |             continue
 31 |         end
 32 |         vname, vdef = m.captures[1:2]
 33 |         push!(assignments, (String(vname) => String(vdef)))
 34 |     end
 35 | 
 36 |     # if in config file, update `GLOBAL_VARS` and return
 37 |     rpath = splitext(locvar("fd_rpath"))[1]
 38 |     if isconfig
 39 |         set_vars!(GLOBAL_VARS, assignments)
 40 |         return nothing
 41 |     end
 42 | 
 43 |     # otherwise set local vars
 44 |     set_vars!(LOCAL_VARS, assignments)
 45 |     rpath = splitext(locvar("fd_rpath"))[1]
 46 | 
 47 |     hasmath = locvar(:hasmath)
 48 |     hascode = locvar(:hascode)
 49 |     if !hascode && globvar("autocode")
 50 |         # check and set hascode automatically
 51 |         code = any(b -> startswith(string(b.name), "CODE_BLOCK"), blocks)
 52 |         set_var!(LOCAL_VARS, "hascode", code)
 53 |     end
 54 |     if !hasmath && globvar("automath")
 55 |         # check and set hasmath automatically
 56 |         math = any(b -> b.name in MATH_BLOCKS_NAMES, blocks)
 57 |         set_var!(LOCAL_VARS, "hasmath", math)
 58 |     end
 59 | 
 60 |     # copy the page vars to ALL_PAGE_VARS so that they can be accessed
 61 |     # by other pages via `pagevar`.
 62 |     ALL_PAGE_VARS[rpath] = deepcopy(LOCAL_VARS)
 63 | 
 64 |     (:process_mddefs, "assignments done & copy to ALL_PAGE_VARS") |> logger
 65 | 
 66 |     # TAGS
 67 |     tags = Set(unique(locvar("tags")))
 68 |     # Cases:
 69 |     # 0. there was no page tags before
 70 |     #   a. tags is empty --> do nothing
 71 |     #   b. tags is not empty --> initialise global page tags + add rpath=>tags
 72 |     # 1. that page did not have tags
 73 |     #   a. tags is empty --> do nothing
 74 |     #   b. tags is not empty register them and update all
 75 |     # 2. that page did have tags
 76 |     #   a. tags are unchanged --> do nothing
 77 |     #   b. check which ones change and update those
 78 |     PAGE_TAGS = globvar("fd_page_tags")
 79 |     if isnothing(PAGE_TAGS)
 80 |         isempty(tags) && return nothing
 81 |         set_var!(GLOBAL_VARS, "fd_page_tags", DTAG((rpath => tags,)))
 82 |     elseif !haskey(PAGE_TAGS, rpath)
 83 |         isempty(tags) && return nothing
 84 |         PAGE_TAGS[rpath] = tags
 85 |     else
 86 |         old_tags = PAGE_TAGS[rpath]
 87 |         if isempty(tags)
 88 |             delete!(PAGE_TAGS, rpath)
 89 |         else
 90 |             PAGE_TAGS[rpath] = tags
 91 |         end
 92 |         # we will need to update all tag pages that were or are related to
 93 |         # the current rpath, indeed properties of the page may have changed
 94 |         # and affect these derived pages (that's why it's a union here
 95 |         # and not a setdiff).
 96 |         tags = union(old_tags, tags)
 97 |     end
 98 |     # In the full pass each page is processed first (without generating tag
 99 |     # pages) and then, when all tags have been gathered, generate_tag_pages
100 |     # is called (see `fd_fullpass`).
101 |     # During the serve loop, we want to trigger on page change.
102 |     pagevar || FD_ENV[:FULL_PASS] || generate_tag_pages(tags)
103 |     return nothing
104 | end
105 | 


--------------------------------------------------------------------------------
/test/manager/utils.jl:
--------------------------------------------------------------------------------
  1 | fs1()
  2 | 
  3 | temp_config = joinpath(F.PATHS[:src], "config.md")
  4 | write(temp_config, raw"""
  5 |     @def author = "Stefan Zweig"
  6 |     @def automath = false
  7 |     @def autocode = false
  8 |     """)
  9 | temp_index = joinpath(F.PATHS[:src], "index.md")
 10 | write(temp_index, "blah blah")
 11 | temp_index2 = joinpath(F.PATHS[:src], "index.html")
 12 | write(temp_index2, "blah blih")
 13 | temp_blah = joinpath(F.PATHS[:src_pages], "blah.md")
 14 | write(temp_blah, "blah blah")
 15 | temp_html = joinpath(F.PATHS[:src_pages], "temp.html")
 16 | write(temp_html, "some html")
 17 | temp_rnd = joinpath(F.PATHS[:src_pages], "temp.rnd")
 18 | write(temp_rnd, "some random")
 19 | temp_css = joinpath(F.PATHS[:src_css], "temp.css")
 20 | write(temp_css, "some css")
 21 | 
 22 | F.process_config()
 23 | 
 24 | @testset "Prep outdir" begin
 25 |     F.prepare_output_dir()
 26 |     @test isdir(F.PATHS[:pub])
 27 |     @test isdir(F.PATHS[:css])
 28 |     temp_out = joinpath(F.PATHS[:pub], "tmp.html")
 29 |     write(temp_out, "This is a test page.\n")
 30 |     # clear is false => file should remain
 31 |     F.prepare_output_dir(false)
 32 |     @test isfile(temp_out)
 33 |     # clear is true => file should go
 34 |     F.prepare_output_dir(true)
 35 |     @test !isfile(temp_out)
 36 | end
 37 | 
 38 | @testset "Scan dir" begin
 39 |     println("🐝 Testing file tracking...:")
 40 |     # it also tests add_if_new_file and last
 41 |     md_files = Dict{Pair{String, String}, Float64}()
 42 |     html_files = empty(md_files)
 43 |     other_files = empty(md_files)
 44 |     infra_files = empty(md_files)
 45 |     literate_files = empty(md_files)
 46 |     watched_files = [other_files, infra_files, md_files, html_files, literate_files]
 47 |     F.scan_input_dir!(other_files, infra_files, md_files, html_files, literate_files, true)
 48 |     @test haskey(md_files, F.PATHS[:src_pages]=>"blah.md")
 49 |     @test md_files[F.PATHS[:src_pages]=>"blah.md"] == mtime(temp_blah) == stat(temp_blah).mtime
 50 |     @test html_files[F.PATHS[:src_pages]=>"temp.html"] == mtime(temp_html)
 51 |     @test other_files[F.PATHS[:src_pages]=>"temp.rnd"] == mtime(temp_rnd)
 52 | end
 53 | 
 54 | @testset "Config+write" begin
 55 |     F.process_config()
 56 |     @test F.GLOBAL_VARS["author"].first == "Stefan Zweig"
 57 |     rm(temp_config)
 58 |     @test_logs (:warn, "I didn't find a config file. Ignoring.") F.process_config()
 59 |     # testing write
 60 |     head = "head"
 61 |     pg_foot = "\npage_foot"
 62 |     foot = "foot {{fill author}}"
 63 | 
 64 |     out_file = F.form_output_path(F.PATHS[:folder], "index.html", :html)
 65 |     F.write_page(F.PATHS[:src], "index.md", head, pg_foot, foot, out_file)
 66 | 
 67 |     @test isfile(out_file)
 68 |     @test isapproxstr(read(out_file, String), """
 69 |         head
 70 |         
71 |

blah blah

72 | page_foot 73 |
74 | foot Stefan Zweig 75 | """) 76 | end 77 | 78 | temp_config = joinpath(F.PATHS[:src], "config.md") 79 | write(temp_config, "@def author = \"Stefan Zweig\"\n") 80 | rm(temp_index2) 81 | 82 | @testset "Part convert" begin # ✅ 16 aug 2018 83 | write(joinpath(F.PATHS[:src_html], "head.html"), raw""" 84 | 85 | 86 | 87 | 88 | 89 | 90 | """) 91 | write(joinpath(F.PATHS[:src_html], "page_foot.html"), raw""" 92 |
93 | 96 |
""") 97 | write(joinpath(F.PATHS[:src_html], "foot.html"), raw""" 98 | 99 | """) 100 | 101 | clear = true 102 | 103 | watched_files = F.fd_setup(; clear=clear) 104 | 105 | F.fd_fullpass(watched_files; clear=clear) 106 | 107 | @test issubset(["css", "libs", "index.html"], readdir(F.PATHS[:folder])) 108 | @test issubset(["temp.html", "temp.rnd"], readdir(F.PATHS[:pub])) 109 | end 110 | 111 | @testset "Err procfile" begin 112 | write(temp_index, "blah blah { blih etc") 113 | println("🐝 Testing error message...:") 114 | @test_throws F.OCBlockError F.process_file_err(:md, F.PATHS[:src] => "index.md"; clear=false) 115 | end 116 | -------------------------------------------------------------------------------- /src/converter/html/html.jl: -------------------------------------------------------------------------------- 1 | """ 2 | Convert a Franklin html string into a html string (i.e. replace `{{ ... }}` 3 | blocks). 4 | """ 5 | function convert_html(hs::AS; isoptim::Bool=false)::String 6 | isempty(hs) && return hs 7 | 8 | (:convert_html, "hasmath/code: $(locvar.((:hasmath, :hascode)))") |> logger 9 | 10 | # Tokenize 11 | tokens = find_tokens(hs, HTML_TOKENS, HTML_1C_TOKENS) 12 | 13 | # Find hblocks ({{ ... }}) 14 | hblocks, tokens = find_all_ocblocks(tokens, HTML_OCB) 15 | deactivate_inner_blocks!(hblocks, (:COMMENT, :SCRIPT)) 16 | filter!(hb -> hb.name ∉ (:COMMENT, :SCRIPT), hblocks) 17 | 18 | # Find qblocks (qualify the hblocks) 19 | qblocks = qualify_html_hblocks(hblocks) 20 | 21 | fhs = process_html_qblocks(hs, qblocks) 22 | 23 | # See issue #204, basically not all markdown links are processed as 24 | # per common mark with the JuliaMarkdown, so this is a patch that kind 25 | # of does 26 | if locvar("reflinks") 27 | fhs = find_and_fix_md_links(fhs) 28 | end 29 | # if it ends with

\n but doesn't start with

, chop it off 30 | # this may happen if the first element parsed is an ocblock (not text) 31 | δ = ifelse(endswith(fhs, "

\n") && !startswith(fhs, "

"), 5, 0) 32 | 33 | isempty(fhs) && return "" 34 | 35 | if !isempty(globvar("prepath")) && isoptim 36 | fhs = fix_links(fhs) 37 | end 38 | 39 | return String(chop(fhs, tail=δ)) 40 | end 41 | 42 | 43 | """ 44 | Return the HTML corresponding to a Franklin-Markdown string as well as all the 45 | page variables. See also [`fd2html`](@ref) which only returns the html. 46 | """ 47 | function fd2html_v(st::AS; internal::Bool=false, 48 | dir::String="")::Tuple{String,Dict} 49 | isempty(st) && return st 50 | if !internal 51 | empty!(ALL_PAGE_VARS) 52 | FOLDER_PATH[] = isempty(dir) ? mktempdir() : dir 53 | set_paths!() 54 | def_GLOBAL_LXDEFS!() 55 | def_GLOBAL_VARS!() 56 | FD_ENV[:CUR_PATH] = "index.md" 57 | end 58 | # corner case if `serve` is used and cleanup has emptied global vars 59 | !(@isdefined GLOBAL_VARS) || isempty(GLOBAL_VARS) && def_GLOBAL_VARS!() 60 | m = convert_md(st; isinternal=internal) 61 | h = convert_html(m) 62 | return h, LOCAL_VARS 63 | end 64 | fd2html(a...; k...)::String = fd2html_v(a...; k...)[1] 65 | 66 | # legacy JuDoc 67 | jd2html = fd2html 68 | 69 | """ 70 | $SIGNATURES 71 | 72 | Take a qualified html block stack and go through it, with recursive calling. 73 | """ 74 | function process_html_qblocks(hs::AS, qblocks::Vector{AbstractBlock}, 75 | head::Int=1, tail::Int=lastindex(hs))::String 76 | htmls = IOBuffer() 77 | head = head # (sub)string index 78 | i = 1 # qualified block index 79 | while i ≤ length(qblocks) 80 | β = qblocks[i] 81 | # write what's before the block 82 | fromβ = from(β) 83 | (head < fromβ) && write(htmls, subs(hs, head, prevind(hs, fromβ))) 84 | 85 | if β isa HTML_OPEN_COND 86 | content, head, i = process_html_cond(hs, qblocks, i) 87 | write(htmls, content) 88 | elseif β isa HFor 89 | content, head, i = process_html_for(hs, qblocks, i) 90 | write(htmls, content) 91 | # should not see an HEnd by itself --> error 92 | elseif β isa HEnd 93 | throw(HTMLBlockError("I found a lonely {{end}}.")) 94 | # it's a function block, process it 95 | else 96 | write(htmls, convert_html_fblock(β)) 97 | head = nextind(hs, to(β)) 98 | end 99 | i += 1 100 | end 101 | # write whatever is left after the last block 102 | head ≤ tail && write(htmls, subs(hs, head, tail)) 103 | return String(take!(htmls)) 104 | end 105 | 106 | """ 107 | match_url(base, cand) 108 | 109 | Try to match two url indicators. 110 | """ 111 | function match_url(base::AS, cand::AS) 112 | sbase = base[1] == '/' ? base[2:end] : base 113 | scand = cand[1] == '/' ? cand[2:end] : cand 114 | # joker-style syntax 115 | if endswith(scand, "/*") 116 | return startswith(sbase, scand[1:prevind(scand, lastindex(scand), 2)]) 117 | elseif endswith(scand, "/") 118 | scand = scand[1:prevind(scand, lastindex(scand))] 119 | end 120 | return splitext(scand)[1] == sbase 121 | end 122 | -------------------------------------------------------------------------------- /test/converter/lx/input.jl: -------------------------------------------------------------------------------- 1 | fs1() 2 | 3 | @testset "LX input" begin 4 | set_curpath("index.md") 5 | mkpath(joinpath(F.PATHS[:assets], "index", "code", "output")) 6 | write(joinpath(F.PATHS[:assets], "index", "code", "s1.jl"), "println(1+1)") 7 | write(joinpath(F.PATHS[:assets], "index", "code", "output", "s1a.png"), "blah") 8 | write(joinpath(F.PATHS[:assets], "index", "code", "output", "s1.out"), "blih") 9 | st = raw""" 10 | Some string 11 | \input{julia}{s1.jl} 12 | Then maybe 13 | \output{s1.jl} 14 | Finally img: 15 | \input{plot:a}{s1.jl} 16 | done. 17 | """; 18 | 19 | F.def_GLOBAL_VARS!() 20 | F.def_GLOBAL_LXDEFS!() 21 | 22 | m = F.convert_md(st) 23 | h = F.convert_html(m) 24 | 25 | @test occursin("

Some string

$(read(joinpath(F.PATHS[:assets], "index", "code", "s1.jl"), String))
", h) 26 | @test occursin("Then maybe
$(read(joinpath(F.PATHS[:assets], "index", "code",  "output", "s1.out"), String))
", h) 27 | @test occursin("Finally img: \"\" done.", h) 28 | end 29 | 30 | @testset "Input MD" begin 31 | mkpath(joinpath(F.PATHS[:assets], "ccc")) 32 | fp = joinpath(F.PATHS[:assets], "ccc", "asset1.md") 33 | write(fp, "blah **blih**") 34 | st = raw""" 35 | Some string 36 | \textinput{ccc/asset1} 37 | """ 38 | @test isapproxstr(st |> conv, "

Some string blah blih

") 39 | end 40 | 41 | 42 | fs2() 43 | 44 | @testset "LX input" begin 45 | set_curpath("index.md") 46 | mkpath(joinpath(F.PATHS[:site], "assets", "index", "code", "output")) 47 | write(joinpath(F.PATHS[:site], "assets", "index", "code", "s1.jl"), "println(1+1)") 48 | write(joinpath(F.PATHS[:site], "assets", "index", "code", "output", "s1a.png"), "blah") 49 | write(joinpath(F.PATHS[:site], "assets", "index", "code", "output", "s1.out"), "blih") 50 | st = raw""" 51 | Some string 52 | \input{julia}{s1.jl} 53 | Then maybe 54 | \output{s1.jl} 55 | Finally img: 56 | \input{plot:a}{s1.jl} 57 | done. 58 | """; 59 | 60 | F.def_GLOBAL_VARS!() 61 | F.def_GLOBAL_LXDEFS!() 62 | 63 | m = F.convert_md(st) 64 | h = F.convert_html(m) 65 | 66 | @test occursin("

Some string

$(read(joinpath(F.PATHS[:site], "assets", "index", "code", "s1.jl"), String))
", h) 67 | @test occursin("Then maybe
$(read(joinpath(F.PATHS[:site], "assets", "index", "code",  "output", "s1.out"), String))
", h) 68 | @test occursin("Finally img: \"\" done.", h) 69 | end 70 | 71 | @testset "Input MD" begin 72 | mkpath(joinpath(F.PATHS[:site], "assets", "ccc")) 73 | fp = joinpath(F.PATHS[:site], "assets", "ccc", "asset1.md") 74 | write(fp, "blah **blih**") 75 | st = raw""" 76 | Some string 77 | \textinput{ccc/asset1} 78 | """ 79 | @test isapproxstr(st |> conv, "

Some string blah blih

") 80 | end 81 | 82 | @testset "Input err" begin 83 | gotd() 84 | s = raw""" 85 | AA 86 | \input{julia}{foo/baz} 87 | \input{plot}{foo/baz} 88 | \textinput{foo/bar} 89 | """ |> fd2html_td 90 | @test isapproxstr(s, """ 91 |

AA 92 |

// Couldn't find a file when trying to resolve an input request with relative path: `foo/baz`. //

93 |

// Couldn't find an output directory associated with 'foo/baz' when trying to input a plot. //

94 |

// Couldn't find a file when trying to resolve an input request with relative path: `foo/bar`. //

95 | """) 96 | 97 | fs2() 98 | gotd() 99 | # table input 100 | mkpath(joinpath(td, "_assets", "index", "output")) 101 | write(joinpath(td, "_assets", "index", "output", "foo.csv"), "bar") 102 | s = raw""" 103 | @def fd_rpath = "index.md" 104 | \tableinput{}{./foo.csv} 105 | """ |> fd2html_td 106 | @test isapproxstr(s, """ 107 |

// Table matching '/assets/index/foo.csv' not found. //

108 | """) 109 | end 110 | -------------------------------------------------------------------------------- /test/parser/2-blocks.jl: -------------------------------------------------------------------------------- 1 | tok = s -> F.find_tokens(s, F.MD_TOKENS, F.MD_1C_TOKENS) 2 | vfn = s -> (t = tok(s); F.validate_footnotes!(t); t) 3 | vh = s -> (t = vfn(s); F.validate_headers!(t); t) 4 | fib = s -> (t = vh(s); F.find_indented_blocks!(t, s); t) 5 | fib2 = s -> (t = fib(s); F.filter_lr_indent!(t, s); t) 6 | 7 | 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)) 8 | blk2 = s -> ((b, t) = blk(s); F.merge_indented_blocks!(b, s); b) 9 | blk3 = s -> (b = blk2(s); F.filter_indented_blocks!(b); b) 10 | 11 | blk4 = s -> (b = blk3(s); F.validate_and_store_link_defs!(b); b) 12 | 13 | isblk(b, n, s) = b.name == n && b.ss == s 14 | cont(b) = F.content(b) 15 | 16 | # Basics 17 | @testset "P:2:blk" begin 18 | b, t = raw""" 19 | A 23 | Then ```julia 1+5``` and ~~~ ~~~. 24 | """ |> blk 25 | @test length(b) == 3 26 | @test isblk(b[1], :COMMENT, "") 27 | @test isblk(b[2], :CODE_BLOCK_LANG, "```julia 1+5```") 28 | @test cont(b[2]) == " 1+5" 29 | @test isblk(b[3], :ESCAPE, "~~~ ~~~") 30 | end 31 | 32 | # with indentation 33 | @testset "P:2:blk-ind" begin 34 | b, t = raw""" 35 | A 36 | 37 | B1 38 | B2 39 | B3 40 | @@d1 41 | B 42 | @@ 43 | """ |> blk 44 | @test length(b) == 2 45 | @test isblk(b[1], :CODE_BLOCK_IND, "\n B1\n B2\n B3\n") 46 | @test isblk(b[2], :DIV, "@@d1\n B\n@@") 47 | 48 | b, t = raw""" 49 | @@d1 50 | @@d2 51 | B 52 | @@ 53 | @@ 54 | """ |> blk 55 | @test length(b) == 1 56 | @test isblk(b[1], :DIV, "@@d1\n @@d2\n B\n @@\n@@") 57 | 58 | b, t = raw""" 59 | @@d1 60 | @@d2 61 | B 62 | @@ 63 | @@ 64 | """ |> blk 65 | @test length(b) == 1 66 | @test isblk(b[1], :DIV, "@@d1\n @@d2\n B\n @@\n@@") 67 | end 68 | 69 | # with indentation and grouping and filtering 70 | @testset "P:2:blk-indF" begin 71 | b = raw""" 72 | A 73 | 74 | B1 75 | B2 76 | 77 | B3 78 | C 79 | """ |> blk3 80 | @test isblk(b[1], :CODE_BLOCK_IND, "\n B1\n B2\n\n B3\n") 81 | 82 | b = raw""" 83 | A 84 | 85 | B 86 | C 87 | D 88 | E 89 | 90 | F 91 | 92 | G 93 | 94 | 95 | 96 | H 97 | """ |> blk3 98 | @test length(b) == 2 99 | @test isblk(b[1], :CODE_BLOCK_IND, "\n B\n C\n D\n E\n") 100 | @test isblk(b[2], :CODE_BLOCK_IND, "\n G\n\n\n\n H\n") 101 | 102 | b = raw""" 103 | @@d1 104 | 105 | A 106 | B 107 | C 108 | @@ 109 | """ |> blk3 110 | @test length(b) == 1 111 | @test isblk(b[1], :DIV, "@@d1\n\n A\n B\n C\n@@") 112 | 113 | b = raw""" 114 | @@d1 115 | @@d2 116 | @@d3 117 | B 118 | @@ 119 | @@ 120 | @@ 121 | """ |> blk3 122 | @test b[1].name == :DIV 123 | end 124 | 125 | @testset "More ind" begin 126 | b = "A\n\n\tB1\n\tB2\n\n\tB3\nC" |> blk3 127 | @test isblk(b[1], :CODE_BLOCK_IND, "\n\tB1\n\tB2\n\n\tB3\n") 128 | end 129 | 130 | @testset "P:2:blk-{}" begin 131 | b = "{ABC}" |> blk3 132 | @test F.content(b[1]) == "ABC" 133 | b = "\\begin{eqnarray} \\sin^2(x)+\\cos^2(x) &=& 1\\end{eqnarray}" |> blk3 134 | @test cont(b[1]) == " \\sin^2(x)+\\cos^2(x) &=& 1" 135 | b = raw""" 136 | a\newcommand{\eqa}[1]{\begin{eqnarray}#1\end{eqnarray}}b@@d .@@ 137 | \eqa{\sin^2(x)+\cos^2(x) &=& 1} 138 | """ |> blk3 139 | @test isblk(b[1], :LXB, raw"{\eqa}") 140 | @test isblk(b[2], :LXB, raw"{\begin{eqnarray}#1\end{eqnarray}}") 141 | @test isblk(b[3], :LXB, raw"{\sin^2(x)+\cos^2(x) &=& 1}") 142 | end 143 | 144 | # links 145 | @testset "P:2:blk-[]" begin 146 | b = """ 147 | A [A] B. 148 | [A]: http://example.com""" |> blk4 149 | @test length(b) == 1 150 | @test isblk(b[1], :LINK_DEF, "[A]: http://example.com") 151 | b = """ 152 | A [A][B] C. 153 | [B]: http://example.com""" |> blk4 154 | @test isblk(b[1], :LINK_DEF, "[B]: http://example.com") 155 | b = """ 156 | A [`B`] C 157 | [`B`]: http://example.com""" |> blk4 158 | @test length(b) == 2 159 | @test isblk(b[1], :CODE_INLINE, "`B`") 160 | @test isblk(b[2], :LINK_DEF, "[`B`]: http://example.com") 161 | end 162 | -------------------------------------------------------------------------------- /test/manager/utils_fs2.jl: -------------------------------------------------------------------------------- 1 | fs2() 2 | 3 | temp_config = joinpath(F.PATHS[:folder], "config.md") 4 | write(temp_config, raw""" 5 | @def author = "Stefan Zweig" 6 | @def automath = false 7 | @def autocode = false 8 | """) 9 | temp_index = joinpath(F.PATHS[:folder], "index.md") 10 | write(temp_index, "blah blah") 11 | temp_index2 = joinpath(F.PATHS[:folder], "index.html") 12 | write(temp_index2, "blah blih") 13 | temp_blah = joinpath(F.PATHS[:folder], "blah.md") 14 | write(temp_blah, "blah blah") 15 | temp_html = joinpath(F.PATHS[:folder], "temp.html") 16 | write(temp_html, "some html") 17 | temp_rnd = joinpath(F.PATHS[:folder], "temp.rnd") 18 | write(temp_rnd, "some random") 19 | temp_css = joinpath(F.PATHS[:css], "temp.css") 20 | write(temp_css, "some css") 21 | temp_js = joinpath(F.PATHS[:libs], "foo.js") 22 | write(temp_js, "some js") 23 | 24 | F.process_config() 25 | 26 | @testset "Prep outdir" begin 27 | F.prepare_output_dir() 28 | @test isdir(F.PATHS[:site]) 29 | temp_out = joinpath(F.PATHS[:site], "tmp.html") 30 | write(temp_out, "This is a test page.\n") 31 | # clear is false => file should remain 32 | F.prepare_output_dir(false) 33 | @test isfile(temp_out) 34 | # clear is true => file should go 35 | F.prepare_output_dir(true) 36 | @test !isfile(temp_out) 37 | end 38 | 39 | @testset "Scan dir" begin 40 | println("🐝 Testing file tracking...:") 41 | # it also tests add_if_new_file and last 42 | md_files = Dict{Pair{String, String}, Float64}() 43 | html_files = empty(md_files) 44 | other_files = empty(md_files) 45 | infra_files = empty(md_files) 46 | literate_files = empty(md_files) 47 | watched_files = [other_files, infra_files, md_files, html_files, literate_files] 48 | F.scan_input_dir!(other_files, infra_files, md_files, html_files, literate_files, true) 49 | @test haskey(md_files, F.PATHS[:folder]=>"blah.md") 50 | @test md_files[F.PATHS[:folder]=>"blah.md"] == mtime(temp_blah) == stat(temp_blah).mtime 51 | @test html_files[F.PATHS[:folder]=>"temp.html"] == mtime(temp_html) 52 | @test other_files[F.PATHS[:folder]=>"temp.rnd"] == mtime(temp_rnd) 53 | end 54 | 55 | @testset "Config+write" begin 56 | F.process_config() 57 | @test F.GLOBAL_VARS["author"].first == "Stefan Zweig" 58 | rm(temp_config) 59 | @test_logs (:warn, "I didn't find a config file. Ignoring.") F.process_config() 60 | # testing write 61 | head = "head" 62 | pg_foot = "\npage_foot" 63 | foot = "foot {{fill author}}" 64 | 65 | out_file = F.form_output_path(F.PATHS[:site], "index.html", :html) 66 | F.write_page(F.PATHS[:folder], "index.md", head, pg_foot, foot, out_file) 67 | 68 | @test isfile(out_file) 69 | @test isapproxstr(read(out_file, String), """ 70 | head\n
\n

blah blah

\n\n\npage_foot\n
\nfoot Stefan Zweig""") 71 | end 72 | 73 | temp_config = joinpath(F.PATHS[:folder], "config.md") 74 | write(temp_config, "@def author = \"Stefan Zweig\"\n") 75 | rm(temp_index2) 76 | 77 | @testset "Part convert" begin 78 | write(joinpath(F.PATHS[:layout], "head.html"), raw""" 79 | 80 | 81 | 82 | 83 | 84 | 85 | """) 86 | write(joinpath(F.PATHS[:layout], "page_foot.html"), raw""" 87 |
88 | 91 |
""") 92 | write(joinpath(F.PATHS[:layout], "foot.html"), raw""" 93 | 94 | """) 95 | 96 | clear = true 97 | 98 | watched_files = F.fd_setup(; clear=clear) 99 | 100 | F.fd_fullpass(watched_files; clear=clear) 101 | 102 | @test issubset(["css", "libs", "index.html"], readdir(F.PATHS[:site])) 103 | @test issubset(["temp", "temp.rnd"], readdir(F.PATHS[:site])) 104 | @test issubset(["index.html"], readdir(joinpath(F.PATHS[:site], "temp"))) 105 | @test issubset(["index.html"], readdir(joinpath(F.PATHS[:site], "blah"))) 106 | end 107 | 108 | @testset "Err procfile" begin 109 | write(temp_index, "blah blah { blih etc") 110 | println("🐝 Testing error message...:") 111 | @test_throws F.OCBlockError F.process_file_err(:md, F.PATHS[:folder] => "index.md"; clear=false) 112 | end 113 | -------------------------------------------------------------------------------- /test/converter/md/markdown.jl: -------------------------------------------------------------------------------- 1 | @testset "Partial MD" begin 2 | st = raw""" 3 | \newcommand{\com}{HH} 4 | \newcommand{\comb}[1]{HH#1HH} 5 | A list 6 | * \com and \comb{blah} 7 | * $f$ is a function 8 | * a last element 9 | """ 10 | 11 | steps = explore_md_steps(st) 12 | lxdefs, tokens, braces, blocks, lxcoms = steps[:latex] 13 | 14 | @test length(braces) == 1 15 | @test F.content(braces[1]) == "blah" 16 | 17 | @test length(blocks) == 1 18 | @test blocks[1].name == :MATH_A 19 | @test F.content(blocks[1]) == "f" 20 | 21 | b2insert, = steps[:b2insert] 22 | 23 | inter_md, mblocks = F.form_inter_md(st, b2insert, lxdefs) 24 | @test inter_md == "\n\nA list\n* ##FDINSERT## and ##FDINSERT## \n* ##FDINSERT## is a function\n* a last element\n" 25 | inter_html = F.md2html(inter_md) 26 | @test inter_html == "

A list

\n
    \n
  • ##FDINSERT## and ##FDINSERT##

    \n
  • \n
  • ##FDINSERT## is a function

    \n
  • \n
  • a last element

    \n
  • \n
\n" 27 | end 28 | 29 | 30 | # index arithmetic over a string is a bit trickier when using all symbols 31 | # we can use `prevind` and `nextind` to make sure it works properly 32 | @testset "Inter Md 2" begin 33 | st = raw""" 34 | ~~~ 35 | this⊙ then ⊙ ⊙ and 36 | ~~~ 37 | finally ⊙⊙𝛴⊙ and 38 | ~~~ 39 | escape ∀⊙∀ 40 | ~~~ 41 | done 42 | """ 43 | 44 | inter_md, = explore_md_steps(st)[:inter_md] 45 | @test inter_md == " ##FDINSERT## \nfinally ⊙⊙𝛴⊙ and\n ##FDINSERT## \ndone\n" 46 | end 47 | 48 | 49 | @testset "Latex eqa" begin 50 | st = raw""" 51 | a\newcommand{\eqa}[1]{\begin{eqnarray}#1\end{eqnarray}}b@@d .@@ 52 | \eqa{\sin^2(x)+\cos^2(x) &=& 1} 53 | """ 54 | 55 | steps = explore_md_steps(st) 56 | lxdefs, tokens, braces, blocks, lxcoms = steps[:latex] 57 | b2insert, = steps[:b2insert] 58 | inter_md, mblocks = steps[:inter_md] 59 | @test inter_md == "ab ##FDINSERT## \n ##FDINSERT## \n" 60 | 61 | inter_html, = steps[:inter_html] 62 | 63 | @test F.convert_block(b2insert[1], lxdefs) == "
.
" 64 | @test isapproxstr(F.convert_block(b2insert[2], lxdefs), "\\[\\begin{array}{c} \\sin^2(x)+\\cos^2(x) &=& 1\\end{array}\\]") 65 | hstring = F.convert_inter_html(inter_html, b2insert, lxdefs) 66 | @test isapproxstr(hstring, raw""" 67 |

68 | ab

.
69 | \[\begin{array}{c} 70 | \sin^2(x)+\cos^2(x) &=& 1 71 | \end{array}\] 72 |

""") 73 | end 74 | 75 | 76 | @testset "MD>HTML" begin 77 | st = raw""" 78 | text A1 \newcommand{\com}{blah}text A2 \com and 79 | ~~~ 80 | escape B1 81 | ~~~ 82 | \newcommand{\comb}[ 1]{\mathrm{#1}} text C1 $\comb{b}$ text C2 83 | \newcommand{\comc}[ 2]{part1:#1 and part2:#2} then \comc{AA}{BB}. 84 | """ 85 | 86 | steps = explore_md_steps(st) 87 | lxdefs, tokens, braces, blocks, lxcoms = steps[:latex] 88 | b2insert, = steps[:b2insert] 89 | inter_md, mblocks = steps[:inter_md] 90 | inter_html, = steps[:inter_html] 91 | 92 | @test isapproxstr(inter_md, """ 93 | text A1 text A2 ##FDINSERT## and 94 | ##FDINSERT## 95 | text C1 ##FDINSERT## text C2 96 | then ##FDINSERT## .""") 97 | 98 | @test isapproxstr(inter_html, """

text A1 text A2 ##FDINSERT## and ##FDINSERT## text C1 ##FDINSERT## text C2 then ##FDINSERT## .

""") 99 | 100 | hstring = F.convert_inter_html(inter_html, b2insert, lxdefs) 101 | @test isapproxstr(hstring, """ 102 |

text A1 text A2 blah and 103 | escape B1 104 | text C1 \\(\\mathrm{ b}\\) text C2 105 | then part1: AA and part2: BB.

""") 106 | end 107 | 108 | 109 | @testset "headers" begin 110 | set_curpath("index.md") 111 | h = """ 112 | # Title 113 | and then 114 | ## Subtitle cool! 115 | done 116 | """ |> seval 117 | @test isapproxstr(h, """ 118 |

Title

119 | and then 120 |

Subtitle cool!

121 | done 122 | """) 123 | end 124 | -------------------------------------------------------------------------------- /test/parser/1-tokenize.jl: -------------------------------------------------------------------------------- 1 | tok = s -> F.find_tokens(s, F.MD_TOKENS, F.MD_1C_TOKENS) 2 | vfn = s -> (t = tok(s); F.validate_footnotes!(t); t) 3 | vh = s -> (t = vfn(s); F.validate_headers!(t); t) 4 | fib = s -> (t = vh(s); F.find_indented_blocks!(t, s); t) 5 | fib2 = s -> (t = fib(s); F.filter_lr_indent!(t, s); t) 6 | 7 | islr(t) = t.name == :LINE_RETURN && t.ss == "\n" 8 | istok(t, n, s) = t.name == n && t.ss == s 9 | isind(t) = t.name == :LR_INDENT && t.ss == "\n " 10 | 11 | ## 12 | ## FIND_TOKENS 13 | ## 14 | 15 | @testset "P:1:find-tok" begin 16 | t = raw""" 17 | @def v = 5 18 | @@da 19 | @@db 20 | @@ 21 | @@ 22 | $A$ and \[B\] and \com{hello} etc 23 | """ |> tok 24 | @test istok(t[1], :MD_DEF_OPEN, "@def") 25 | @test islr(t[2]) 26 | @test istok(t[3], :DIV_OPEN, "@@da") 27 | @test islr(t[4]) 28 | @test istok(t[5], :DIV_OPEN, "@@db") 29 | @test islr(t[6]) 30 | @test istok(t[7], :DIV_CLOSE, "@@") 31 | @test islr(t[8]) 32 | @test istok(t[9], :DIV_CLOSE, "@@") 33 | @test islr(t[10]) 34 | @test istok(t[11], :MATH_A, "\$") 35 | @test istok(t[12], :MATH_A, "\$") 36 | @test istok(t[13], :MATH_C_OPEN, "\\[") 37 | @test istok(t[14], :MATH_C_CLOSE, "\\]") 38 | @test istok(t[15], :LX_COMMAND, "\\com") 39 | @test istok(t[16], :LXB_OPEN, "{") 40 | @test istok(t[17], :LXB_CLOSE, "}") 41 | @test islr(t[18]) 42 | @test istok(t[19], :EOS, "\n") 43 | 44 | # complement div with `-` or `_` 45 | t = raw"""A @@d-a B@@ C""" |> tok 46 | @test istok(t[1], :DIV_OPEN, "@@d-a") 47 | t = raw"""A @@d-1 B@@ C""" |> tok 48 | @test istok(t[1], :DIV_OPEN, "@@d-1") 49 | end 50 | 51 | @testset "P:1:ctok" begin 52 | # check that tokens at EOS close properly 53 | # NOTE: this was avoided before by the addition of a special char 54 | # to denote the end of the string but we don't do that anymore. 55 | # see `isexactly` and `find_tokens` in the fixed pattern case. 56 | t = "@@d ... @@" |> tok 57 | @test istok(t[1], :DIV_OPEN, "@@d") 58 | @test istok(t[2], :DIV_CLOSE, "@@") 59 | @test istok(t[3], :EOS, "@") 60 | t = "``` ... ```" |> tok 61 | @test istok(t[1], :CODE_TRIPLE, "```") 62 | @test istok(t[2], :CODE_TRIPLE, "```") 63 | @test istok(t[3], :EOS, "`") 64 | t = "" |> tok 65 | @test istok(t[1], :COMMENT_OPEN, "") 67 | t = "~~~...~~~" |> tok 68 | @test istok(t[1], :ESCAPE, "~~~") 69 | @test istok(t[2], :ESCAPE, "~~~") 70 | t = "b `j`" |> tok 71 | @test istok(t[1], :CODE_SINGLE, "`") 72 | @test istok(t[2], :CODE_SINGLE, "`") 73 | @test istok(t[3], :EOS, "`") 74 | end 75 | 76 | ## 77 | ## VALIDATE_FOOTNOTE! 78 | ## 79 | 80 | @testset "P:1:val-fn" begin 81 | t = raw""" 82 | A [^B] and 83 | [^B]: etc 84 | """ |> vfn 85 | @test istok(t[1], :FOOTNOTE_REF, "[^B]") 86 | @test islr(t[2]) 87 | @test istok(t[3], :FOOTNOTE_DEF, "[^B]:") 88 | end 89 | 90 | ## 91 | ## VALIDATE_HEADERS! 92 | ## 93 | 94 | @testset "P:1:val-hd" begin 95 | t = raw""" 96 | # A 97 | ## B 98 | and # C 99 | """ |> vh 100 | @test istok(t[1], :H1_OPEN, "#") 101 | @test islr(t[2]) 102 | @test istok(t[3], :H2_OPEN, "##") 103 | @test islr(t[4]) 104 | @test islr(t[5]) 105 | @test istok(t[6], :EOS, "\n") 106 | end 107 | 108 | ## 109 | ## FIND_INDENTED_BLOCKS! 110 | ## 111 | 112 | @testset "P:1:fib" begin 113 | s = raw""" 114 | A 115 | 116 | B1 117 | B2 118 | B3 119 | C 120 | @@da 121 | @@db 122 | E 123 | @@ 124 | @@ 125 | E 126 | F 127 | G 128 | """ 129 | t = s |> fib 130 | @test islr(t[1]) 131 | @test isind(t[2]) && t[2].lno == 3 132 | @test isind(t[3]) && t[3].lno == 4 133 | @test isind(t[4]) && t[4].lno == 5 # B3 134 | @test islr(t[5]) 135 | @test islr(t[6]) 136 | @test istok(t[7], :DIV_OPEN, "@@da") 137 | @test isind(t[8]) && t[8].lno == 8 138 | @test istok(t[9], :DIV_OPEN, "@@db") 139 | @test isind(t[10]) && t[10].lno == 9 # in front of E 140 | @test isind(t[11]) && t[11].lno == 10 # in front of @@ 141 | @test istok(t[12], :DIV_CLOSE, "@@") 142 | @test islr(t[13]) 143 | @test istok(t[14], :DIV_CLOSE, "@@") 144 | @test islr(t[15]) 145 | @test isind(t[16]) && t[16].lno == 13 # F 146 | @test islr(t[17]) 147 | @test islr(t[18]) 148 | @test istok(t[19], :EOS, "\n") 149 | 150 | t = s |> fib2 151 | @test length(t) == 19 152 | 153 | @test isind.([t[2], t[3], t[4]]) |> all 154 | @test islr.([t[8], t[10], t[11], t[16]]) |> all 155 | end 156 | -------------------------------------------------------------------------------- /test/global/eval.jl: -------------------------------------------------------------------------------- 1 | # This set of tests is specifically for the ordering 2 | # of operations, when code blocks are eval-ed or re-eval-ed 3 | 4 | @testset "EvalOrder" begin 5 | flush_td(); set_globals() 6 | 7 | Random.seed!(0) # seed for extra testing of when code is ran 8 | # Generate random numbers in a sequence then we'll check 9 | # things are done in the same sequence on the page 10 | a, b, c, d, e = randn(5) 11 | Random.seed!(0) 12 | 13 | # Create a page "foo.md" which we'll use and abuse 14 | foo = raw""" 15 | @def hascode = true 16 | ```julia:ex 17 | println(randn()) 18 | ``` 19 | \output{ex} 20 | """ 21 | 22 | # XXX FIRST PASS --> EVAL 23 | 24 | h = foo |> fd2html_td 25 | 26 | @test isapproxstr(h, """ 27 |
println(randn())
$a
28 | """) 29 | 30 | # XXX TEXT MODIFICATION + IN SCOPE --> NO REEVAL 31 | 32 | foo *= "etc" 33 | 34 | h = foo |> fd2html_td 35 | 36 | @test isapproxstr(h, """ 37 |
println(randn())
$a
etc 38 | """) 39 | 40 | # XXX CODE ADDITION --> NO REEVAL OF FIRST BLOCK 41 | 42 | foo *= raw""" 43 | ```julia:ex2 44 | println(randn()) 45 | ``` 46 | \output{ex2} 47 | ```julia:ex3 48 | println(randn()) 49 | ``` 50 | \output{ex3} 51 | """ 52 | 53 | h = foo |> fd2html_td 54 | 55 | @test isapproxstr(h, """ 56 |
println(randn())
$a
etc 57 |
println(randn())
$b
58 |
println(randn())
$c
59 | """) 60 | 61 | # XXX CODE MODIFICATION --> REEVAL OF BLOCK AND AFTER 62 | 63 | foo = raw""" 64 | @def hascode = true 65 | ```julia:ex 66 | println(randn()) 67 | ``` 68 | \output{ex} 69 | ```julia:ex2 70 | # modif 71 | println(randn()) 72 | ``` 73 | \output{ex2} 74 | ```julia:ex3 75 | println(randn()) 76 | ``` 77 | \output{ex3} 78 | """ 79 | 80 | h = foo |> fd2html_td 81 | 82 | @test isapproxstr(h, """ 83 |
println(randn())
$a
84 |
# modif
 85 |                 println(randn())
$d
86 |
println(randn())
$e
87 | """) 88 | end 89 | 90 | @testset "EvalOrder2" begin 91 | flush_td(); set_globals() 92 | 93 | # Inserting new code block 94 | h = raw""" 95 | @def hascode = true 96 | ```julia:ex1 97 | a = 5 98 | println(a) 99 | ``` 100 | \output{ex1} 101 | ```julia:ex2 102 | a += 3 103 | println(a) 104 | ``` 105 | \output{ex2} 106 | """ |> fd2html_td 107 | 108 | @test isapproxstr(h, """ 109 |
a = 5
110 |             println(a)
111 |
5
112 |
a += 3
113 |             println(a)
114 |
8
115 | """) 116 | 117 | h = raw""" 118 | @def hascode = true 119 | @def reeval = true 120 | ```julia:ex1 121 | a = 5 122 | println(a) 123 | ``` 124 | \output{ex1} 125 | ```julia:ex1b 126 | a += 1 127 | println(a) 128 | ``` 129 | \output{ex1b} 130 | ```julia:ex2 131 | a += 3 132 | println(a) 133 | ``` 134 | \output{ex2} 135 | """ |> fd2html_td 136 | 137 | @test isapproxstr(h, """ 138 |
a = 5
139 |             println(a)
140 |
5
141 |
a += 1
142 |             println(a)
143 |
6
144 |
a += 3
145 |             println(a)
146 |
9
147 | """) 148 | end 149 | -------------------------------------------------------------------------------- /test/utils/errors.jl: -------------------------------------------------------------------------------- 1 | s = """Veggies es bonus vobis, proinde vos postulo essum magis kohlrabi welsh onion daikon amaranth tatsoi tomatillo melon azuki bean garlic. 2 | 3 | Gumbo beet greens corn soko endive gumbo gourd. Parsley shallot courgette tatsoi pea sprouts fava bean collard greens dandelion okra wakame tomato. Dandelion cucumber earthnut pea peanut soko zucchini. 4 | 5 | Turnip greens yarrow ricebean rutabaga endive cauliflower sea lettuce kohlrabi amaranth water spinach avocado daikon napa cabbage asparagus winter purslane kale. 6 | Celery potato scallion desert raisin horseradish spinach carrot soko. Lotus root water spinach fennel kombu maize bamboo shoot green bean swiss chard seakale pumpkin onion chickpea gram corn pea. Brussels sprout coriander water chestnut gourd swiss chard wakame kohlrabi beetroot carrot watercress. 7 | Corn amaranth salsify bunya nuts nori azuki bean chickweed potato bell pepper artichoke. 8 | """ 9 | 10 | @testset "context" begin 11 | mess = F.context(s, 101) 12 | @test s[101] == 't' 13 | # println(mess) 14 | @test mess == "Context:\n\t...ikon amaranth tatsoi tomatillo melon azuki ... (near line 1)\n ^---\n" 15 | 16 | mess = F.context(s, 211) 17 | @test s[211] == 't' 18 | # println(mess) 19 | @test mess == "Context:\n\t...ey shallot courgette tatsoi pea sprouts fav... (near line 2)\n ^---\n" 20 | 21 | mess = F.context(s, 10) 22 | # println(mess) 23 | @test mess == "Context:\n\tVeggies es bonus vobis, proinde... (near line 1)\n ^---\n" 24 | 25 | mess = F.context(s, 880) 26 | # println(mess) 27 | @test mess == "Context:\n\t... potato bell pepper artichoke. (near line 5)\n ^---\n" 28 | end 29 | 30 | @testset "show" begin 31 | ocbe = F.OCBlockError("foo", "bar") 32 | io = IOBuffer() 33 | Base.showerror(io, ocbe) 34 | r = String(take!(io)) 35 | @test r == "foo\nbar" 36 | 37 | mcbe = F.MathBlockError("foo") 38 | @test mcbe.m == "foo" 39 | end 40 | 41 | @testset "ocbe" begin 42 | s = raw""" 43 | Foo $$ end. 44 | """ 45 | @test_throws F.OCBlockError s |> fd2html_td 46 | end 47 | 48 | @testset "lxbe" begin 49 | s = raw""" 50 | Foo 51 | \newcommand{\hello} 52 | End 53 | """ 54 | @test_throws F.LxDefError s |> fd2html_td 55 | 56 | s = raw""" 57 | Foo 58 | \newcommand{\hello} {goodbye} 59 | \hello 60 | End 61 | """ |> fd2html_td 62 | @test isapproxstr(s, "

Foo

goodbye End

") 63 | 64 | s2 = raw""" 65 | Foo 66 | \newcommand{\hello}[ ]{goodbye} 67 | \hello 68 | End 69 | """ |> fd2html_td 70 | @test isapproxstr(s, s2) 71 | 72 | # should fail XXX 73 | 74 | s2 = raw""" 75 | Foo 76 | \newcommand{\hello}b{goodbye} 77 | \hello 78 | End 79 | """ 80 | @test_throws F.LxDefError s2 |> fd2html_td 81 | 82 | s2 = raw""" 83 | Foo 84 | \newcommand{\hello}b{goodbye #1} 85 | \hello{ccc} 86 | End 87 | """ 88 | @test_throws F.LxDefError s2 |> fd2html_td 89 | 90 | s2 = raw""" 91 | Foo 92 | \newcommand{\hello}[bb]{goodbye #1} 93 | \hello{ccc} 94 | End 95 | """ 96 | @test_throws F.LxDefError s2 |> fd2html_td 97 | 98 | # tolerated 99 | 100 | s2 = raw""" 101 | Foo 102 | \newcommand{\hello} {goodbye} 103 | \hello 104 | End 105 | """ |> fd2html_td 106 | @test isapproxstr(s, s2) 107 | end 108 | 109 | @testset "Unfound" begin 110 | fs2(); 111 | @test_throws F.FileNotFoundError F.resolve_rpath("./foo") 112 | end 113 | 114 | @testset "H-For" begin 115 | s = """ 116 | {{for x in blah}} 117 | foo 118 | """ 119 | @test_throws F.HTMLBlockError s |> F.convert_html 120 | s = """ 121 | @def list = [1,2,3] 122 | ~~~ 123 | {{for x in list}} 124 | foo 125 | ~~~ 126 | """ 127 | @test_throws F.HTMLBlockError s |> fd2html_td 128 | s = """ 129 | @def list = [1,2,3] 130 | ~~~ 131 | {{for x in list2}} 132 | foo 133 | {{end}} 134 | ~~~ 135 | """ 136 | @test_throws F.HTMLBlockError s |> fd2html_td 137 | s = """ 138 | @def list = [1,2,3] 139 | ~~~ 140 | {{for x in list}} 141 | {{if a}} 142 | foo 143 | {{end}} 144 | ~~~ 145 | """ 146 | @test_throws F.HTMLBlockError s |> fd2html_td 147 | end 148 | 149 | @testset "HToc" begin 150 | s = """ 151 | {{toc 1}} 152 | """ 153 | @test_throws F.HTMLFunctionError s |> F.convert_html 154 | s = """ 155 | ~~~ 156 | {{toc aa bb}} 157 | ~~~ 158 | # Hello 159 | """ 160 | @test_throws F.HTMLFunctionError s |> fd2html_td 161 | end 162 | -------------------------------------------------------------------------------- /src/converter/markdown/tags.jl: -------------------------------------------------------------------------------- 1 | """ 2 | $(SIGNATURES) 3 | 4 | Create and/or update `__site/tag` folders/files. It takes the set of tags to 5 | refresh (in which case only the pages associated to those tags will be 6 | refreshed) or an empty set in which case all tags will be (re)-generated. 7 | Note that if a tag is removed from a page, it may be that `refresh_tags` 8 | contains a tag which should effectively be removed because no page have it. 9 | (This only matters at the end in the call to `write_tag_page`). 10 | Note: the `clean_tags` cleans up orphan tags (tags that wouldn't be pointing 11 | to any page anymore). 12 | """ 13 | function generate_tag_pages(refresh_tags=Set{String}())::Nothing 14 | # if there are no page tags, cleanup and finish 15 | PAGE_TAGS = globvar("fd_page_tags") 16 | isnothing(PAGE_TAGS) && return clean_tags() 17 | # if there are page tags, eliminiate the pages that don't exist anymore 18 | for rpath in keys(PAGE_TAGS) 19 | isfile(rpath * ".md") || delete!(PAGE_TAGS, rpath) 20 | end 21 | # if there's nothing left, clean up and finish 22 | isempty(PAGE_TAGS) && return clean_tags() 23 | 24 | # Here we have a non-empty PAGE_TAGS dictionary where the keys are 25 | # paths of existing files, and values are the tags associated with 26 | # these files. 27 | # Get the inverse dictionary {tag -> [rp1, rp2...]} 28 | TAG_PAGES = invert_dict(PAGE_TAGS) 29 | # store it in globvar 30 | set_var!(GLOBAL_VARS, "fd_tag_pages", TAG_PAGES) 31 | all_tags = collect(keys(TAG_PAGES)) 32 | 33 | # some tags may have been given to refresh which don't have any 34 | # pages linking to them anymore, these tags will have to be cleaned up 35 | rm_tags = filter(t -> t ∉ all_tags, refresh_tags) 36 | 37 | # check which tags should be refreshed 38 | update_tags = isempty(refresh_tags) ? all_tags : 39 | filter(t -> t ∈ all_tags, refresh_tags) 40 | 41 | # Generate the tag folder if it doesn't exist already 42 | isdir(path(:tag)) || mkpath(path(:tag)) 43 | # Generate the tag layout page if it doesn't exist (it should...) 44 | isfile(joinpath(path(:layout), "tag.html")) || write_default_tag_layout() 45 | # write each tag, note that they are necessarily in TAG_PAGES 46 | for tag in update_tags 47 | write_tag_page(tag) 48 | end 49 | return clean_tags(rm_tags) 50 | end 51 | 52 | """ 53 | clean_tags(rm_tags=nothing) 54 | 55 | Check the content of the tag folder and remove orphan pages. 56 | """ 57 | function clean_tags(rm_tags=nothing)::Nothing 58 | isdir(path(:tag)) || return nothing 59 | PAGE_TAGS = globvar("fd_page_tags") 60 | if isnothing(PAGE_TAGS) || isempty(PAGE_TAGS) 61 | all_tags = [] 62 | else 63 | all_tags = union(values(PAGE_TAGS)...) 64 | end 65 | # check what folders are present 66 | all_tag_folders = readdir(path(:tag)) 67 | # check what folders shouldn't be present 68 | orphans = setdiff(all_tags, all_tag_folders) 69 | if !isnothing(rm_tags) 70 | orphans = union(orphans, rm_tags) 71 | end 72 | # clean up 73 | for dirname in orphans 74 | dirpath = joinpath(path(:tag), dirname) 75 | isdir(dirpath) && rm(dirpath, recursive=true) 76 | end 77 | return nothing 78 | end 79 | 80 | """ 81 | $SIGNATURES 82 | 83 | Internal function to (re)write the tag pages corresponding to `update_tags`. 84 | """ 85 | function write_tag_page(tag)::Nothing 86 | # make `fd_tag` available to that page generation 87 | # also allows {{istag tagname}} ... {{end}} 88 | set_var!(LOCAL_VARS, "fd_tag", tag) 89 | 90 | layout_key = ifelse(FD_ENV[:STRUCTURE] < v"0.2", :src_html, :layout) 91 | layout = path(layout_key) 92 | content = read(joinpath(layout, "tag.html"), String) 93 | 94 | dir = joinpath(path(:tag), tag) 95 | isdir(dir) || mkdir(dir) 96 | 97 | write(joinpath(dir, "index.html"), convert_html(content)) 98 | 99 | # reset `fd_tag` 100 | set_var!(LOCAL_VARS, "fd_tag", "") 101 | return nothing 102 | end 103 | 104 | 105 | """ 106 | write_default_tag_layout() 107 | 108 | If `_layout/tag.html` is not defined, input a basic default one indicating 109 | that the user has to modify it. This will help users transition when they may 110 | have used a template that did not define `_layout/tag.html`. 111 | """ 112 | function write_default_tag_layout()::Nothing 113 | html = """ 114 | 115 | 116 | 117 | 118 | 119 | Tag: {{fill fd_tag}} 120 | 121 | 122 |
123 | {{taglist}} 124 |
125 | 126 | 127 | """ 128 | write(joinpath(path(:layout), "tag.html"), html) 129 | return nothing 130 | end 131 | -------------------------------------------------------------------------------- /src/eval/codeblock.jl: -------------------------------------------------------------------------------- 1 | #= 2 | Functionalities to take a code block and process it. 3 | These functionalities are called from `convert/md_blocks`. 4 | =# 5 | 6 | """ 7 | $SIGNATURES 8 | 9 | Take a fenced code block and return a tuple with the language, the relative 10 | path (if any) and the code. 11 | """ 12 | function parse_fenced_block(ss::SubString)::Tuple 13 | # cases: (note there's necessarily a lang, see `convert_block`) 14 | # * ```lang ... ``` where lang can be something like julia-repl 15 | # * ```lang:path ... ``` where path is a relative path like "this/path" 16 | # group 1 => lang; group 2 => path; group 3 => code 17 | reg = ifelse(startswith(ss, "`````"), CODE_5_PAT, CODE_3_PAT) 18 | m = match(reg, ss) 19 | lang = m.captures[1] 20 | rpath = m.captures[2] 21 | code = strip(m.captures[3]) 22 | rpath === nothing || (rpath = rpath[2:end]) # ignore starting `:` 23 | return lang, rpath, code 24 | end 25 | 26 | """ 27 | $SIGNATURES 28 | 29 | Return true if the code should be reevaluated and false otherwise. 30 | The code should be reevaluated if any of following flags are true: 31 | 32 | 1. global eval flag (`eval_all`) 33 | 1. local eval flag (`reeval`) 34 | 1. stale scope (due to a prior code block having been evaluated) 35 | 1. the code is different than what's in the script 36 | 1. the output is missings 37 | """ 38 | function should_eval(code::AS, rpath::AS) 39 | # 1. global setting forcing all pages to reeval 40 | FD_ENV[:FORCE_REEVAL] && return true 41 | 42 | # 2. local setting forcing the current page to reeval everything 43 | locvar("reeval") && return true 44 | 45 | # 3. if space previously marked as stale, return true 46 | # note that on every page build, this is re-init as false. 47 | locvar("fd_eval") && return true 48 | 49 | # 4. if the code has changed reeval 50 | cp = form_codepaths(rpath) 51 | # >> does the script exist? 52 | 53 | isfile(cp.script_path) || return true 54 | # >> does the script match the code? 55 | MESSAGE_FILE_GEN_FMD * code == read(cp.script_path, String) || return true 56 | 57 | # 5. if the outputs aren't there, reeval 58 | # >> does the output dir exist 59 | isdir(cp.out_dir) || return true 60 | # >> do the output files exist? 61 | all(isfile, (cp.out_path, cp.res_path)) || return true 62 | 63 | # otherwise don't reeval 64 | return false 65 | end 66 | 67 | """ 68 | $SIGNATURES 69 | 70 | Helper function to process the content of a code block. 71 | Return the html corresponding to the code block, possibly after having 72 | evaluated the code. 73 | """ 74 | function resolve_code_block(ss::SubString)::String 75 | # 1. what kind of code is it 76 | lang, rpath, code = parse_fenced_block(ss) 77 | # 1.a if no rpath is given, code should not be evaluated 78 | isnothing(rpath) && return html_code(code, lang) 79 | # 1.b if not julia code, eval is not supported 80 | if lang != "julia" 81 | @warn "Evaluation of non-Julia code blocks is not yet supported." 82 | return html_code(code, lang) 83 | end 84 | 85 | # NOTE: in future if support direct eval of code in other 86 | # languages, can't do the module trick so will need to keep track 87 | # of that virtually. There will need to be a branching over lang=="julia" 88 | # vs rest here. 89 | 90 | # 2. here we have Julia code, assess whether to run it or not 91 | # if not, just return the code as a html block 92 | if should_eval(code, rpath) 93 | # 3. here we have code that should be (re)evaluated 94 | # >> retrieve the modulename, the module may not exist 95 | # (& may not need to) 96 | modname = modulename(locvar("fd_rpath")) 97 | # >> check if relevant module exists, otherwise create one 98 | mod = ismodule(modname) ? 99 | getfield(Main, Symbol(modname)) : 100 | newmodule(modname) 101 | # >> retrieve the code paths 102 | cp = form_codepaths(rpath) 103 | # >> write the code to file 104 | mkpath(cp.script_dir) 105 | write(cp.script_path, MESSAGE_FILE_GEN_FMD * code) 106 | # make the output directory available to the code block 107 | # (see @OUTPUT macro) 108 | OUT_PATH[] = cp.out_dir 109 | # >> eval the code in the relevant module (this creates output/) 110 | res = run_code(mod, code, cp.out_path; strip_code=false) 111 | # >> write res to file 112 | open(cp.res_path, "w") do resf 113 | redirect_stdout(resf) do 114 | show(stdout, "text/plain", res) 115 | end 116 | end 117 | # >> since we've evaluated a code block, toggle scope as stale 118 | set_var!(LOCAL_VARS, "fd_eval", true) 119 | end 120 | # >> finally return as html 121 | if locvar("showall") 122 | return html_code(code, lang) * 123 | reprocess("\\show{$rpath}", [GLOBAL_LXDEFS["\\show"]]) 124 | end 125 | return html_code(code, lang) 126 | end 127 | -------------------------------------------------------------------------------- /src/regexes.jl: -------------------------------------------------------------------------------- 1 | #= ===================================================== 2 | LATEX patterns, see html link fixer, validate_footnotes 3 | ===================================================== =# 4 | 5 | """ 6 | LX_NAME_PAT 7 | 8 | Regex to find the name in a new command within a brace block. For example: 9 | 10 | \\newcommand{\\com}[2]{def} 11 | 12 | will give as first capture group `\\com`. 13 | """ 14 | const LX_NAME_PAT = r"^\s*(\\[\p{L}]+)\s*$" 15 | 16 | 17 | """ 18 | LX_NARG_PAT 19 | 20 | Regex to find the number of argument in a new command (if it is given). For 21 | example: 22 | 23 | \\newcommand{\\com}[2]{def} 24 | 25 | will give as second capture group `2`. If there are no number of arguments, the 26 | second capturing group will be `nothing`. 27 | """ 28 | const LX_NARG_PAT = r"\s*(\[\s*(\d)\s*\])?\s*" 29 | 30 | #= ===================================================== 31 | MDDEF patterns 32 | ===================================================== =# 33 | """ 34 | ASSIGN_PAT 35 | 36 | Regex to match 'var' in an assignment of the form 37 | 38 | var = value 39 | """ 40 | const ASSIGN_PAT = r"^([\p{L}_]\S*)\s*?=((?:.|\n)*)" 41 | 42 | #= ===================================================== 43 | LINK patterns, see html link fixer, validate_footnotes 44 | ===================================================== =# 45 | # here we're looking for [id] or [id][] or [stuff][id] or ![stuff][id] but not [id]: 46 | # 1 > (!)? == either ! or nothing 47 | # 2 > [(.*?)] == [...] inside of the brackets 48 | # 3 > (?:[(.*?)])? == [...] inside of second brackets if there is such 49 | const ESC_LINK_PAT = r"(!)?[(.*?)](?!:)(?:[(.*?)])?" 50 | 51 | const FN_DEF_PAT = r"^\[\^[\p{L}0-9_]+\](:)?$" 52 | 53 | #= ===================================================== 54 | CODE blocks 55 | ===================================================== =# 56 | 57 | const CODE_3_PAT = Regex( 58 | "```([a-zA-Z][a-zA-Z-]*)" * # language 59 | "(?:(" * # optional script name 60 | "\\:[\\p{L}\\\\\\/_\\.]" * # :(...) start of script name 61 | "[\\p{L}_0-9-\\\\\\/]+" * # script name 62 | "(?:\\.[a-zA-Z0-9]+)?" * # script extension 63 | ")|(?:\\n|\\s))" * 64 | "\\s*\\n?((?:.|\\n)*)```") # rest of the code 65 | 66 | const CODE_5_PAT = Regex("``" * CODE_3_PAT.pattern * "``") 67 | 68 | #= ===================================================== 69 | HTML entity pattern 70 | ===================================================== =# 71 | const HTML_ENT_PAT = r"&(?:[a-z0-9]+|#[0-9]{1,6}|#x[0-9a-f]{1,6});" 72 | 73 | #= ===================================================== 74 | HBLOCK patterns, see html blocks 75 | NOTE: the { is { and 125 is }, this is because 76 | Markdown.html converts { => html entity but we want to 77 | recognise those double as {{ so that they can be used 78 | within markdown as well. 79 | NOTE: the for block needs verification (matching parens) 80 | ===================================================== =# 81 | const HBO = raw"(?:{{|{{)\s*" 82 | const HBC = raw"\s*(?:}}|}})" 83 | const VAR = raw"([\p{L}_]\S*)" 84 | const ANY = raw"((.|\n)+?)" 85 | 86 | const HBLOCK_IF_PAT = Regex(HBO * raw"if\s+" * VAR * HBC) 87 | const HBLOCK_ELSE_PAT = Regex(HBO * "else" * HBC) 88 | const HBLOCK_ELSEIF_PAT = Regex(HBO * raw"else\s*if\s+" * VAR * HBC) 89 | const HBLOCK_END_PAT = Regex(HBO * "end" * HBC) 90 | 91 | const HBLOCK_ISDEF_PAT = Regex(HBO * raw"i(?:s|f)def\s+" * VAR * HBC) 92 | const HBLOCK_ISNOTDEF_PAT = Regex(HBO * raw"i(?:s|f)n(?:ot)?def\s+" * VAR * HBC) 93 | 94 | const HBLOCK_ISPAGE_PAT = Regex(HBO * raw"ispage\s+" * ANY * HBC) 95 | const HBLOCK_ISNOTPAGE_PAT = Regex(HBO * raw"isnotpage\s+" * ANY * HBC) 96 | 97 | """ 98 | HBLOCK_FOR_PAT 99 | 100 | Regex to match `{{ for v in iterate }}` or {{ for (v1, v2) in iterate}} etc 101 | where `iterate` is an iterator 102 | """ 103 | const HBLOCK_FOR_PAT = Regex( 104 | HBO * raw"for\s+" * 105 | raw"(\(?(?:\s*[\p{L}_][^\r\n\t\f\v,]*,\s*)*[\p{L}_]\S*\s*\)?)" * 106 | raw"\s+in\s+" * VAR * HBC) 107 | 108 | """ 109 | HBLOCK_FUN_PAT 110 | 111 | Regex to match `{{ fname param₁ param₂ }}` where `fname` is a html processing 112 | function and `paramᵢ` should refer to appropriate variables in the current 113 | scope. 114 | """ 115 | const HBLOCK_FUN_PAT = Regex(HBO * VAR * raw"(\s+((.|\n)*?))?" * HBC) 116 | 117 | #= ===================================================== 118 | Pattern checkers 119 | ===================================================== =# 120 | 121 | """ 122 | check_for_pat(v) 123 | 124 | Check that we have something like `{{for v in iterate}}` or 125 | `{for (v1,v2) in iterate}}` but not something with unmached parens. 126 | """ 127 | function check_for_pat(v) 128 | op = startswith(v, "(") 129 | cp = endswith(v, ")") 130 | xor(op, cp) && 131 | throw(HTMLBlockError("Unbalanced expression in {{for ...}}")) 132 | !op && occursin(",", v) && 133 | throw(HTMLBlockError("Missing parens in {{for ...}}")) 134 | return nothing 135 | end 136 | -------------------------------------------------------------------------------- /src/converter/html/prerender.jl: -------------------------------------------------------------------------------- 1 | """ 2 | $(SIGNATURES) 3 | 4 | Takes a html string that may contain inline katex blocks `\\(...\\)` or display katex blocks 5 | `\\[ ... \\]` and use node and katex to pre-render them to HTML. 6 | """ 7 | function js_prerender_katex(hs::String)::String 8 | # look for \(, \) and \[, \] (we know they're paired because generated from markdown parsing) 9 | matches = collect(eachmatch(r"\\(\(|\[|\]|\))", hs)) 10 | isempty(matches) && return hs 11 | 12 | # buffer to write the JS script 13 | jsbuffer = IOBuffer() 14 | write(jsbuffer, """ 15 | const katex = require("$(escape_string(joinpath(PATHS[:libs], "katex", "katex.min.js")))"); 16 | """) 17 | # string to separate the output of the different blocks 18 | splitter = "_>fdsplit<_" 19 | 20 | # go over each match and add the content to the jsbuffer 21 | for i ∈ 1:2:length(matches)-1 22 | # tokens are paired, no nesting 23 | mo, mc = matches[i:i+1] 24 | # check if it's a display style 25 | display = (mo.match == "\\[") 26 | # this is the content without the \( \) or \[ \] 27 | ms = subs(hs, nextind(hs, mo.offset + 1), prevind(hs, mc.offset)) 28 | # add to content of jsbuffer 29 | write(jsbuffer, """ 30 | var html = katex.renderToString("$(escape_string(ms))", {displayMode: $display}) 31 | console.log(html) 32 | """) 33 | # in between every block, write $splitter so that output can be split easily 34 | i == length(matches)-1 || write(jsbuffer, """\nconsole.log("$splitter")\n""") 35 | end 36 | return js2html(hs, jsbuffer, matches, splitter) 37 | end 38 | 39 | 40 | """ 41 | $(SIGNATURES) 42 | 43 | Takes a html string that may contain `
` blocks and use node and 44 | highlight.js to pre-render them to HTML. 45 | """ 46 | function js_prerender_highlight(hs::String)::String 47 | # look for "
" these will have been automatically generated 48 | # and therefore the regex can be fairly strict with spaces etc 49 | matches = collect(eachmatch(r"
|<\/code><\/pre>", hs))
 50 |     isempty(matches) && return hs
 51 | 
 52 |     # buffer to write the JS script
 53 |     jsbuffer = IOBuffer()
 54 |     write(jsbuffer, """const hljs = require('highlight.js');""")
 55 | 
 56 |     # string to separate the output of the different blocks
 57 |     splitter = "_>fdsplit<_"
 58 | 
 59 |     # go over each match and add the content to the jsbuffer
 60 |     for i ∈ 1:2:length(matches)-1
 61 |         # tokens are paired, no nesting
 62 |         co, cc = matches[i:i+1]
 63 |         # core code
 64 |         cs = subs(hs, nextind(hs, matchrange(co).stop), prevind(hs, matchrange(cc).start))
 65 |         cs = escape_string(cs)
 66 |         # lang
 67 |         lang = co.captures[2]
 68 |         if isnothing(lang)
 69 |             write(jsbuffer, """console.log("
$cs
");\n""") 70 | else 71 | if lang == "html" && is_html_escaped(cs) 72 | # corner case, see Franklin.jl/issues/326 73 | # highlight will re-escape the string further. 74 | cs = html_unescape(cs) |> escape_string 75 | end 76 | # add to content of jsbuffer 77 | write(jsbuffer, """console.log("
" + hljs.highlight("$lang", "$cs").value + "
");""") 78 | end 79 | # in between every block, write $splitter so that output can be split easily 80 | i == length(matches)-1 || write(jsbuffer, """console.log('$splitter');""") 81 | end 82 | return js2html(hs, jsbuffer, matches, splitter) 83 | end 84 | 85 | 86 | """ 87 | $(SIGNATURES) 88 | 89 | Convenience function to run the content of `jsbuffer` with node, and lace the results with `hs`. 90 | """ 91 | function js2html(hs::String, jsbuffer::IOBuffer, matches::Vector{RegexMatch}, 92 | splitter::String)::String 93 | # run it redirecting the output to a buffer 94 | outbuffer = IOBuffer() 95 | run(pipeline(`$NODE -e "$(String(take!(jsbuffer)))"`, stdout=outbuffer)) 96 | 97 | # read the buffer and split it using $splitter 98 | out = String(take!(outbuffer)) 99 | parts = split(out, splitter) 100 | 101 | # lace everything back together 102 | htmls = IOBuffer() 103 | head, c = 1, 1 104 | for i ∈ 1:2:length(matches)-1 105 | mo, mc = matches[i:i+1] 106 | write(htmls, subs(hs, head, prevind(hs, mo.offset))) 107 | pp = strip(parts[c]) 108 | if startswith(pp, "
"shell>")
110 |             pp = replace(pp, r"(\(.*?\)) pkg>"=>s"\1 pkg>")
111 |         end
112 |         write(htmls, pp)
113 |         head = mc.offset + length(mc.match)
114 |         c += 1
115 |     end
116 |     # add the rest of the document beyond the last mathblock
117 |     head < lastindex(hs) && write(htmls, subs(hs, head, lastindex(hs)))
118 | 
119 |     return String(take!(htmls))
120 | end
121 | 


--------------------------------------------------------------------------------
/src/parser/html/tokens.jl:
--------------------------------------------------------------------------------
  1 | """
  2 | HTML_1C_TOKENS
  3 | 
  4 | Dictionary of single-char tokens for HTML. Note that these characters are
  5 | exclusive, they cannot appear again in a larger token.
  6 | """
  7 | const HTML_1C_TOKENS = LittleDict{Char, Symbol}()
  8 | 
  9 | 
 10 | """
 11 | HTML_TOKENS
 12 | 
 13 | Dictionary of tokens for HTML. Note that for each, there may be several
 14 | possibilities to consider in which case the order is important: the first case
 15 | that works will be taken.
 16 | """
 17 | const HTML_TOKENS = LittleDict{Char, Vector{TokenFinder}}(
 18 |     '<' => [ isexactly("")  => :COMMENT_CLOSE ],  #      ... -->
 24 |     '{' => [ isexactly("{{")   => :H_BLOCK_OPEN  ],  # {{
 25 |     '}' => [ isexactly("}}")   => :H_BLOCK_CLOSE ],  # }}
 26 |     ) # end dict
 27 | 
 28 | """
 29 | HTML_OCB
 30 | 
 31 | List of HTML Open-Close blocks.
 32 | """
 33 | const HTML_OCB = [
 34 |     # name        opening token    closing token(s)     nestable
 35 |     # ----------------------------------------------------------
 36 |     OCProto(:COMMENT, :COMMENT_OPEN, (:COMMENT_CLOSE,), false),
 37 |     OCProto(:SCRIPT,  :SCRIPT_OPEN,  (:SCRIPT_CLOSE,),  false),
 38 |     OCProto(:H_BLOCK, :H_BLOCK_OPEN, (:H_BLOCK_CLOSE,), true)
 39 |     ]
 40 | 
 41 | #= ===============
 42 | CONDITIONAL BLOCKS
 43 | ================== =#
 44 | 
 45 | #= NOTE / TODO
 46 | * in a conditional block, should make sure else is not followed by elseif
 47 | * no nesting of conditional blocks is allowed at the moment. This could
 48 | be done at later stage (needs balancing) or something but seems a bit overkill
 49 | at this point. This second point might fix the first one by making sure that
 50 |     HIf -> HElseIf / HElse / HEnd
 51 |     HElseIf -> HElseIf / HElse / HEnd
 52 |     HElse -> HEnd
 53 | =#
 54 | 
 55 | """
 56 | $(TYPEDEF)
 57 | 
 58 | HTML token corresponding to `{{if var}}`.
 59 | """
 60 | struct HIf <: AbstractBlock
 61 |     ss::SubString
 62 |     vname::String
 63 | end
 64 | 
 65 | """
 66 | $(TYPEDEF)
 67 | 
 68 | HTML token corresponding to `{{else}}`.
 69 | """
 70 | struct HElse <: AbstractBlock
 71 |     ss::SubString
 72 | end
 73 | 
 74 | """
 75 | $(TYPEDEF)
 76 | 
 77 | HTML token corresponding to `{{elseif var}}`.
 78 | """
 79 | struct HElseIf <: AbstractBlock
 80 |     ss::SubString
 81 |     vname::String
 82 | end
 83 | 
 84 | """
 85 | $(TYPEDEF)
 86 | 
 87 | HTML token corresponding to `{{end}}`.
 88 | """
 89 | struct HEnd <: AbstractBlock
 90 |     ss::SubString
 91 | end
 92 | 
 93 | # -----------------------------------------------------
 94 | # General conditional block based on a boolean variable
 95 | # -----------------------------------------------------
 96 | 
 97 | """
 98 | $(TYPEDEF)
 99 | 
100 | HTML conditional block corresponding to `{{if var}} ... {{else}} ... {{end}}`.
101 | """
102 | struct HCond <: AbstractBlock
103 |     ss::SubString               # full block
104 |     init_cond::String           # initial condition (has to exist)
105 |     sec_conds::Vector{String}   # secondary conditions (can be empty)
106 |     actions::Vector{SubString}  # what to do when conditions are met
107 | end
108 | 
109 | # ------------------------------------------------------------
110 | # Specific conditional block based on whether a var is defined
111 | # ------------------------------------------------------------
112 | 
113 | """
114 | $(TYPEDEF)
115 | 
116 | HTML token corresponding to `{{isdef var}}`.
117 | """
118 | struct HIsDef <: AbstractBlock
119 |     ss::SubString
120 |     vname::String
121 | end
122 | 
123 | 
124 | """
125 | $(TYPEDEF)
126 | 
127 | HTML token corresponding to `{{isnotdef var}}`.
128 | """
129 | struct HIsNotDef <: AbstractBlock
130 |     ss::SubString
131 |     vname::String
132 | end
133 | 
134 | # ------------------------------------------------------------
135 | # Specific conditional block based on whether the current page
136 | # is or isn't in a group of given pages
137 | # ------------------------------------------------------------
138 | 
139 | """
140 | $(TYPEDEF)
141 | 
142 | HTML token corresponding to `{{ispage path/page}}`.
143 | """
144 | struct HIsPage <: AbstractBlock
145 |     ss::SubString
146 |     pages::Vector{<:AS} # one or several pages
147 | end
148 | 
149 | """
150 | $(TYPEDEF)
151 | 
152 | HTML token corresponding to `{{isnotpage path/page}}`.
153 | """
154 | struct HIsNotPage <: AbstractBlock
155 |     ss::SubString
156 |     pages::Vector{<:AS}
157 | end
158 | 
159 | """
160 | $(TYPEDEF)
161 | 
162 | HTML token corresponding to `{{for x in iterable}}`.
163 | """
164 | struct HFor <: AbstractBlock
165 |     ss::SubString
166 |     vname::String
167 |     iname::String
168 | end
169 | 
170 | #= ============
171 | FUNCTION BLOCKS
172 | =============== =#
173 | 
174 | """
175 | $(TYPEDEF)
176 | 
177 | HTML function block corresponding to `{{ fname p1 p2 ...}}`.
178 | """
179 | struct HFun <: AbstractBlock
180 |     ss::SubString
181 |     fname::String
182 |     params::Vector{String}
183 | end
184 | 
185 | 
186 | """
187 | $(TYPEDEF)
188 | 
189 | Empty struct to keep the same taxonomy.
190 | """
191 | struct HToc <: AbstractBlock end
192 | 


--------------------------------------------------------------------------------
/src/converter/markdown/utils.jl:
--------------------------------------------------------------------------------
  1 | """
  2 | $(SIGNATURES)
  3 | 
  4 | Convenience function to call the base markdown to html converter on "simple" strings (i.e. strings
  5 | that don't need to be further considered and don't contain anything else than markdown tokens).
  6 | The boolean `stripp` indicates whether to remove the inserted `

` and `

` by the base markdown 7 | processor, this is relevant for things that are parsed within latex commands etc. 8 | """ 9 | function md2html(ss::AS; stripp::Bool=false)::AS 10 | # if there's nothing, return that... 11 | isempty(ss) && return ss 12 | # Use Julia's Markdown parser followed by Julia's MD->HTML conversion 13 | partial = ss |> fix_inserts |> Markdown.parse 14 | # take over from the parsing of indented blocks 15 | for (i, c) in enumerate(partial.content) 16 | c isa Markdown.Code || continue 17 | partial.content[i] = Markdown.Paragraph(Any[c.code]) 18 | end 19 | # Use Julia's MD->HTML conversion 20 | partial = partial |> Markdown.html 21 | # Markdown.html transforms {{ with HTML entities but we don't want that 22 | partial = replace(partial, r"{{" => "{{") 23 | partial = replace(partial, r"}}" => "}}") 24 | # In some cases, base converter adds

...

\n which we might not want 25 | stripp || return partial 26 | if startswith(partial, "

") && endswith(partial, "

\n") 27 | partial = chop(partial, head=3, tail=5) 28 | end 29 | return partial 30 | end 31 | 32 | 33 | """ 34 | $(SIGNATURES) 35 | 36 | Convenience function to check if `idx` is smaller than the length of `v`, if it is, then return the starting point of `v[idx]` (via `from`), otherwise return `BIG_INT`. 37 | """ 38 | from_ifsmaller(v::Vector, idx::Int, len::Int)::Int = (idx > len) ? BIG_INT : from(v[idx]) 39 | 40 | 41 | """ 42 | $(SIGNATURES) 43 | 44 | Since divs are recursively processed, once they've been found, everything 45 | inside them needs to be deactivated and left for further re-processing to 46 | avoid double inclusion. 47 | """ 48 | function deactivate_divs(blocks::Vector{OCBlock})::Vector{OCBlock} 49 | active_blocks = ones(Bool, length(blocks)) 50 | for (i, β) ∈ enumerate(blocks) 51 | fromβ, toβ = from(β), to(β) 52 | active_blocks[i] || continue 53 | if β.name == :DIV 54 | innerblocks = findall(b -> (fromβ < from(b) < toβ), blocks) 55 | active_blocks[innerblocks] .= false 56 | end 57 | end 58 | return blocks[active_blocks] 59 | end 60 | 61 | 62 | """ 63 | $(SIGNATURES) 64 | 65 | The insertion token have whitespaces around them: ` ##FDINSERT## `, this mostly helps but causes 66 | a problem when combined with italic or bold markdown mode since `_blah_` works but not `_ blah _`. 67 | This function looks for any occurrence of `[\\*_] ##FDINSERT##` or the opposite and removes the 68 | extraneous whitespace. 69 | """ 70 | fix_inserts(s::AS)::String = 71 | replace(replace(s, r"([\*_]) ##FDINSERT##" => s"\1##FDINSERT##"), 72 | r"##FDINSERT## ([\*_])" => s"##FDINSERT##\1") 73 | 74 | """ 75 | $(SIGNATURES) 76 | 77 | Takes a list of tokens and deactivate tokens that happen to be in a multi-line 78 | md-def. Used in [`convert_md`](@ref). 79 | """ 80 | function preprocess_candidate_mddefs!(tokens::Vector{Token}) 81 | isempty(tokens) && return nothing 82 | # process: 83 | # 1. find a MD_DEF_OPEN token 84 | # 2. Look for the first LINE_RETURN (proper) 85 | # 3. try to parse the content with Meta.parse. If it completely fails, 86 | # error, otherwise consider the span of the first ok expression and 87 | # discard all tokens within its span leaving effectively just the 88 | # opening MD_DEF_OPEN and closing LINE_RETURN 89 | from_to_list = Pair{Int,Int}[] 90 | i = 0 91 | while i < length(tokens) 92 | i += 1 93 | τ = tokens[i] 94 | τ.name == :MD_DEF_OPEN || continue 95 | # look ahead stopping with the first LINE_RETURN 96 | j = findfirst(τc -> τc.name ∈ (:LINE_RETURN, :EOS), tokens[i+1:end]) 97 | if j !== nothing 98 | e = i+j 99 | # discard if it's a single line 100 | any(τx -> τx.name == :LR_INDENT, tokens[i:e]) || continue 101 | push!(from_to_list, (i => e)) 102 | end 103 | end 104 | remove = Int[] 105 | s = str(first(tokens)) 106 | for (i, j) in from_to_list 107 | si = nextind(s, to(tokens[i])) 108 | sj = prevind(s, from(tokens[j])) 109 | sc = subs(s, si, sj) 110 | ex, pos = Meta.parse(sc, 1) 111 | 112 | # find where's the first character after the effective end of the 113 | # definition (right-strip) 114 | str_expr = subs(s, si, si + prevind(sc, pos)) 115 | stripped = strip(str_expr) 116 | start_id = findfirst(c -> c == stripped[1], str_expr) 117 | last_id = prevind(str_expr, start_id + length(stripped)) 118 | 119 | # find the first token after si + next_id - 1 and discard what's before 120 | c = findfirst(k -> from(tokens[k]) > si + last_id - 1, i+1:j) 121 | # we discard everything between i+1 and i+c-1 (we want to keep i+c) 122 | if !isnothing(c) 123 | append!(remove, i+1:i+c-1) 124 | end 125 | end 126 | deleteat!(tokens, remove) 127 | return nothing 128 | end 129 | -------------------------------------------------------------------------------- /test/parser/markdown-extra.jl: -------------------------------------------------------------------------------- 1 | @testset "Bold x*" begin # issue 223 2 | h = raw"**x\***" |> seval 3 | @test h == "

x*

\n" 4 | 5 | h = raw"_x\__" |> seval 6 | @test h == "

x_

\n" 7 | end 8 | 9 | @testset "Bold code" begin # issue 222 10 | h = raw"""A **`master`** B.""" |> seval 11 | @test h == "

A master B.

\n" 12 | end 13 | 14 | @testset "Tickssss" begin # issue 219 15 | st = raw"""A `B` C""" 16 | tokens = F.find_tokens(st, F.MD_TOKENS, F.MD_1C_TOKENS) 17 | @test tokens[1].name == :CODE_SINGLE 18 | @test tokens[2].name == :CODE_SINGLE 19 | 20 | st = raw"""A ``B`` C""" 21 | tokens = F.find_tokens(st, F.MD_TOKENS, F.MD_1C_TOKENS) 22 | @test tokens[1].name == :CODE_DOUBLE 23 | @test tokens[2].name == :CODE_DOUBLE 24 | 25 | st = raw"""A ``` B ``` C""" 26 | tokens = F.find_tokens(st, F.MD_TOKENS, F.MD_1C_TOKENS) 27 | @test tokens[1].name == :CODE_TRIPLE 28 | @test tokens[2].name == :CODE_TRIPLE 29 | 30 | st = raw"""A ````` B ````` C""" 31 | tokens = F.find_tokens(st, F.MD_TOKENS, F.MD_1C_TOKENS) 32 | @test tokens[1].name == :CODE_PENTA 33 | @test tokens[2].name == :CODE_PENTA 34 | 35 | st = raw"""A ```b B ``` C""" 36 | tokens = F.find_tokens(st, F.MD_TOKENS, F.MD_1C_TOKENS) 37 | @test tokens[1].name == :CODE_LANG 38 | @test tokens[2].name == :CODE_TRIPLE 39 | 40 | st = raw"""A `````b B ````` C""" 41 | tokens = F.find_tokens(st, F.MD_TOKENS, F.MD_1C_TOKENS) 42 | @test tokens[1].name == :CODE_LANG2 43 | @test tokens[2].name == :CODE_PENTA 44 | 45 | h = raw""" 46 | A 47 | `````markdown 48 | B 49 | ````` 50 | C 51 | """ |> fd2html_td 52 | 53 | @test isapproxstr(h, raw""" 54 |

A 55 |

B
 56 |             
C

57 | """) 58 | 59 | h = raw""" 60 | A 61 | `````markdown 62 | ```julia 63 | B 64 | ``` 65 | ````` 66 | C 67 | """ |> fd2html_td 68 | 69 | @test isapproxstr(h, raw""" 70 |

A 71 |

```julia
 72 |             B
 73 |             ```
 74 |             
C

75 | """) 76 | end 77 | 78 | @testset "Nested ind" begin # issue 285 79 | h = raw""" 80 | \newcommand{\hello}{ 81 | yaya 82 | bar bar 83 | } 84 | \hello 85 | """ |> fd2html_td 86 | @test isapproxstr(h, raw"""yaya bar bar""") 87 | 88 | h = raw""" 89 | @@da 90 | @@db 91 | @@dc 92 | blah 93 | @@ 94 | @@ 95 | @@ 96 | """ |> fd2html_td 97 | @test isapproxstr(h, raw""" 98 |
99 |
100 |
101 | blah 102 |
103 |
104 |
105 | """) 106 | h = raw""" 107 | \newcommand{\hello}[1]{#1} 108 | \hello{ 109 | good lord 110 | } 111 | """ |> fd2html_td 112 | @test isapproxstr(h, "good lord") 113 | end 114 | 115 | @testset "Double brace" begin 116 | s = """ 117 | @def title = "hello" 118 | {{title}}{{title}} 119 | """ |> fd2html_td 120 | @test isapproxstr(s, "hellohello") 121 | s = """ 122 | @def a_b = "hello" 123 | @def c_d = "goodbye" 124 | {{a_b}}{{c_d}} 125 | """ |> fd2html_td 126 | @test isapproxstr(s, "hellogoodbye") 127 | end 128 | 129 | # issue 424 with double braces 130 | @testset "Double brace2" begin 131 | s = raw""" 132 | @def title = "hello" 133 | {{title}} 134 | $\rho=\frac{e^{-\beta \mathcal{E}_{s}}} {\mathcal{Z}} $ 135 | """ |> fd2html_td 136 | @test isapproxstr(s, """ 137 | hello \\(\\rho=\\frac{e^{-\\beta \\mathcal{E}_{s}}} {\\mathcal{Z}} \\)""") 138 | end 139 | 140 | # issue 432 and consequences 141 | @testset "Hz rule" begin 142 | # issue 432 143 | s = raw""" 144 | hello[^a] 145 | 146 | [^a]: world 147 | 148 | --- 149 | """ |> fd2html_td 150 | @test isapproxstr(s, """ 151 |

hello[1] 152 | 153 | 154 | 155 | 156 | 157 |
[1]world
158 |


159 | """) 160 | s = raw""" 161 | A 162 | --- 163 | """ |> fd2html_td 164 | @test isapproxstr(s, """ 165 |

A

""") 166 | s = raw""" 167 | A 168 | ---- 169 | **** 170 | """ |> fd2html_td 171 | @test isapproxstr(s, "

A

\n
") 172 | 173 | # cases where nothing should happen 174 | s = raw""" 175 | A 176 | ---* 177 | ***_ 178 | """ |> fd2html_td 179 | @test isapproxstr(s, "

A –-* ***_

") # note double -- is transformed - 180 | end 181 | 182 | # issue 439 and consequences 183 | @testset "mathunicode" begin 184 | s = raw""" 185 | \newcommand{\u}{1} 186 | $$ φφφ\u abcdef $$ 187 | """ |> fd2html_td 188 | @test isapproxstr(s, raw"\[ φφφ1 abcdef \]") 189 | end 190 | -------------------------------------------------------------------------------- /test/converter/html/html.jl: -------------------------------------------------------------------------------- 1 | @testset "Cblock+h-fill" begin 2 | F.def_GLOBAL_VARS!() 3 | F.def_LOCAL_VARS!() 4 | F.set_vars!(F.LOCAL_VARS, ["v1"=>"\"INPUT1\"", "b1"=>"false", "b2"=>"true"]) 5 | hs = raw""" 6 | Some text then {{ fill v1 }} and 7 | {{ if b1 }} 8 | show stuff here {{ fill v2 }} 9 | {{ else if b2 }} 10 | other stuff 11 | {{ else }} 12 | show other stuff 13 | {{ end }} 14 | final text 15 | """ 16 | @test F.convert_html(hs) == "Some text then INPUT1 and\n\nother stuff\n\nfinal text\n" 17 | 18 | for expr in ("isdef", "ifdef") 19 | hs = """abc {{$expr no}}yada{{end}} def""" 20 | @test F.convert_html(hs) == "abc def" 21 | end 22 | 23 | hs = raw"""abc {{ fill nope }} ... """ 24 | @test (@test_logs (:warn, "I found a '{{fill nope}}' but I do not know the variable 'nope'. Ignoring.") F.convert_html(hs)) == "abc ... " 25 | 26 | hs = raw"""unknown fun {{ unknown fun }} and see.""" 27 | @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." 28 | end 29 | 30 | 31 | @testset "h-insert" begin 32 | fs1() 33 | temp_rnd = joinpath(F.PATHS[:src_html], "temp.rnd") 34 | write(temp_rnd, "some random text to insert") 35 | hs = raw""" 36 | Trying to insert: {{ insert temp.rnd }} and see. 37 | """ 38 | @test F.convert_html(hs) == "Trying to insert: some random text to insert and see.\n" 39 | 40 | hs = raw"""Trying to insert: {{ insert nope.rnd }} and see.""" 41 | @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." 42 | end 43 | 44 | @testset "h-insert-fs2" begin 45 | fs2() 46 | temp_rnd = joinpath(F.PATHS[:layout], "temp.rnd") 47 | write(temp_rnd, "some random text to insert") 48 | hs = raw""" 49 | Trying to insert: {{ insert temp.rnd }} and see. 50 | """ 51 | @test F.convert_html(hs) == "Trying to insert: some random text to insert and see.\n" 52 | 53 | hs = raw"""Trying to insert: {{ insert nope.rnd }} and see.""" 54 | @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." 55 | end 56 | 57 | 58 | @testset "cond-insert" begin 59 | F.set_vars!(F.LOCAL_VARS, [ 60 | "author" => "\"Stefan Zweig\"", 61 | "date_format" => "\"U dd, yyyy\"", 62 | "isnotes" => "true"]) 63 | hs = "foot {{if isnotes}} {{fill author}}{{end}}" 64 | rhs = F.convert_html(hs) 65 | @test rhs == "foot Stefan Zweig" 66 | end 67 | 68 | 69 | @testset "cond-insert 2" begin 70 | F.set_vars!(F.LOCAL_VARS, [ 71 | "author" => "\"Stefan Zweig\"", 72 | "date_format" => "\"U dd, yyyy\"", 73 | "isnotes" => "true"]) 74 | hs = "foot {{isdef author}} {{fill author}}{{end}}" 75 | rhs = F.convert_html(hs) 76 | @test rhs == "foot Stefan Zweig" 77 | for expr in ("isnotdef", "ifnotdef", "isndef", "ifndef") 78 | hs2 = "foot {{$expr blogname}}hello{{end}}" 79 | rhs = F.convert_html(hs2) 80 | @test rhs == "foot hello" 81 | end 82 | end 83 | 84 | @testset "escape-coms" begin 85 | F.set_vars!(F.LOCAL_VARS, [ 86 | "author" => "\"Stefan Zweig\"", 87 | "date_format" => "\"U dd, yyyy\"", 88 | "isnotes" => "true"]) 89 | hs = "foot {{isdef author}} {{fill author}}{{end}}" 90 | rhs = F.convert_html(hs) 91 | @test rhs == "foot Stefan Zweig" 92 | end 93 | 94 | 95 | @testset "Cblock+empty" begin # refers to #96 96 | F.set_vars!(F.LOCAL_VARS, [ 97 | "b1" => "false", 98 | "b2" => "true"]) 99 | 100 | fdc = x->F.convert_html(x) 101 | 102 | # flag b1 is false 103 | @test "{{if b1}} blah {{ else }} blih {{ end }}" |> fdc == " blih " # else 104 | @test "{{if b1}} {{ else }} blih {{ end }}" |> fdc == " blih " # else 105 | 106 | # flag b2 is true 107 | @test "{{if b2}} blah {{ else }} blih {{ end }}" |> fdc == " blah " # if 108 | @test "{{if b2}} blah {{ else }} {{ end }}" |> fdc == " blah " # if 109 | @test "{{if b2}} blah {{ end }}" |> fdc == " blah " # if 110 | 111 | @test "{{if b1}} blah {{ else }} {{ end }}" |> fdc == " " # else, empty 112 | @test "{{if b1}} {{ else }} {{ end }}" |> fdc == " " # else, empty 113 | @test "{{if b1}} blah {{ end }}" |> fdc == "" # else, empty 114 | @test "{{if b2}} {{ else }} {{ end }}" |> fdc == " " # if, empty 115 | @test "{{if b2}} {{ else }} blih {{ end }}" |> fdc == " " # if, empty 116 | end 117 | 118 | @testset "Cond ispage" begin 119 | hs = raw""" 120 | Some text then {{ ispage index.html }} blah {{ end }} but 121 | {{isnotpage blah.html ya/xx}} blih {{end}} done. 122 | """ 123 | 124 | set_curpath("index.md") 125 | @test F.convert_html(hs) == "Some text then blah but\n blih done.\n" 126 | 127 | set_curpath("blah/blih.md") 128 | hs = raw""" 129 | A then {{ispage blah/*}}yes{{end}} but not {{isnotpage blih/*}}no{{end}} E. 130 | """ 131 | @test F.convert_html(hs) == "A then yes but not no E.\n" 132 | 133 | set_curpath("index.md") 134 | end 135 | -------------------------------------------------------------------------------- /test/converter/md/markdown2.jl: -------------------------------------------------------------------------------- 1 | # this follows `markdown.jl` but spurred by bugs/issues 2 | 3 | function inter(st::String) 4 | steps = explore_md_steps(st) 5 | return steps[:inter_md].inter_md, steps[:inter_html].inter_html 6 | end 7 | 8 | @testset "issue163" begin 9 | st = raw"""A _B `C` D_ E""" 10 | imd, ih = inter(st) 11 | @test imd == "A _B ##FDINSERT## D_ E" 12 | @test ih == "

A B ##FDINSERT## D E

\n" 13 | 14 | st = raw"""A _`B` C D_ E""" 15 | imd, ih = inter(st) 16 | @test imd == "A _ ##FDINSERT## C D_ E" 17 | @test ih == "

A ##FDINSERT## C D E

\n" 18 | 19 | st = raw"""A _B C `D`_ E""" 20 | imd, ih = inter(st) 21 | @test imd == "A _B C ##FDINSERT## _ E" 22 | @test ih == "

A B C ##FDINSERT## E

\n" 23 | 24 | st = raw"""A _`B` C `D`_ E""" 25 | imd, ih = inter(st) 26 | @test imd == "A _ ##FDINSERT## C ##FDINSERT## _ E" 27 | @test ih == "

A ##FDINSERT## C ##FDINSERT## E

\n" 28 | end 29 | 30 | 31 | @testset "TOC" begin 32 | fs1() 33 | h = raw""" 34 | @def fd_rpath = "pages/ff/aa.md" 35 | \toc 36 | ## Hello `fd` 37 | #### weirdly nested 38 | ### Goodbye! 39 | ## Done 40 | done. 41 | """ |> seval 42 | @test isapproxstr(h, raw""" 43 |
44 |
    45 |
  1. 46 | Hello fd 47 |
      48 |
      1. weirdly nested
    1. 49 |
    2. Goodbye!
    3. 50 |
    51 |
  2. 52 |
  3. Done
  4. 53 |
54 |
55 |

Hello fd

56 |

weirdly nested

57 |

Goodbye!

58 |

Done

done. 59 | """) 60 | end 61 | 62 | @testset "TOC" begin 63 | fs1() 64 | s = raw""" 65 | @def fd_rpath = "pages/ff/aa.md" 66 | @def mintoclevel = 2 67 | @def maxtoclevel = 3 68 | \toc 69 | # A 70 | ## B 71 | #### C 72 | ### D 73 | ## E 74 | ### F 75 | done. 76 | """ |> seval 77 | @test isapproxstr(s, raw""" 78 |
79 |
    80 |
  1. B 81 |
      82 |
    1. D
    2. 83 |
    84 |
  2. 85 |
  3. E 86 |
      87 |
    1. F
    2. 88 |
    89 |
  4. 90 |
91 |
92 |

A

93 |

B

94 |

C

95 |

D

96 |

E

97 |

F

done. 98 | """) 99 | end 100 | 101 | @testset "TOC-fs2" begin 102 | fs2() 103 | h = raw""" 104 | @def fd_rpath = "pages/ff/aa.md" 105 | \toc 106 | ## Hello `fd` 107 | #### weirdly nested 108 | ### Goodbye! 109 | ## Done 110 | done. 111 | """ |> seval 112 | @test isapproxstr(h, raw""" 113 |
114 |
    115 |
  1. 116 | Hello fd 117 |
      118 |
      1. weirdly nested
    1. 119 |
    2. Goodbye!
    3. 120 |
    121 |
  2. 122 |
  3. Done
  4. 123 |
124 |
125 |

Hello fd

126 |

weirdly nested

127 |

Goodbye!

128 |

Done

done. 129 | """) 130 | end 131 | 132 | @testset "TOC-fs2" begin 133 | fs2() 134 | s = raw""" 135 | @def fd_rpath = "pages/ff/aa.md" 136 | @def mintoclevel = 2 137 | @def maxtoclevel = 3 138 | \toc 139 | # A 140 | ## B 141 | #### C 142 | ### D 143 | ## E 144 | ### F 145 | done. 146 | """ |> seval 147 | @test isapproxstr(s, raw""" 148 |
149 |
    150 |
  1. B 151 |
      152 |
    1. D
    2. 153 |
    154 |
  2. 155 |
  3. E 156 |
      157 |
    1. F
    2. 158 |
    159 |
  4. 160 |
161 |
162 |

A

163 |

B

164 |

C

165 |

D

166 |

E

167 |

F

done. 168 | """) 169 | end 170 | --------------------------------------------------------------------------------