├── .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 = "
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 | B CD ERSS 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 | INI A
31 |1 + 12
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 `"""
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 ABC 
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 
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 | """) 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 |
| [1] | 26 |first footnote | 27 |
| [2] | 32 |second footnote | 33 |
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 | CD
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 | 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
") 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 |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") == "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 = "<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 |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 |A
118 |B [❗️ongoing ]
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 |In julia rational numbers can be constructed with the // operator. Lets define two rational numbers, x and y:
# 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:
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 |In julia rational numbers can be constructed with the // operator. Lets define two rational numbers, x and y:
# 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:
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 |Bardenet, Doucet and Holmes: On Markov Chain Monte Carlo Methods for Tall Data, 2017.
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("""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 |