Repository: tlienart/JuDoc.jl
Pull request #156 (2019-05-28 08:48:26)

(don't merge yet) Eval
Prototype for on-the-fly evaluation of julia code blocks (see #152) 

before merge:

* [x] add more situation tests
  * [x] simple code, no module
  * [x] simple code with module
  * [x] img generation and loading
* [x] ensure no re-eval if code has already been run (test with sleep)
* [x] fix templates in JuDocTemplates to use the new syntax
* [x] open an issue to add this to the docs
  * [x] indicate that code is **not** sandboxed (unlike in weave for instance) and that users are encouraged to be reasonable about these things. In some future there may be sandboxing; for now it's judged a bit overkill
  * [x] now all output files associated with a script must end with `.out` (already the case if generated through code block eval)


Patch:
diff --git a/.travis.yml b/.travis.yml
index 4e7fb6c6..569664be 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,6 +5,7 @@ os:
 julia:
   - 1.0
   - 1.1
+  - 1.2
   - nightly
 matrix:
   allow_failures:
diff --git a/docs/src/index.md b/docs/src/index.md
index 94386ba1..09f496db 100644
--- a/docs/src/index.md
+++ b/docs/src/index.md
@@ -3,10 +3,10 @@
 JuDoc is a simple **static site generator** (SSG) oriented towards technical blogging (code, maths, ...) and light, fast-loading pages.
 The base syntax is plain markdown with a few extensions such as the ability to define and use LaTeX-like commands in or outside of maths environments (see the [Syntax](@ref) section).
 
-For a short list of features see [here](#About-1).
+➡ For a list of the key features see [here](#About-1).
 
 ```@raw html
-For a demo of available templates, see <a href="https://tlienart.github.io/JuDocTemplates.jl/" target="_blank" rel="noopener noreferrer">here</a> (opens in a new tab).
+➡ For a demo of available templates, see <a href="https://tlienart.github.io/JuDocTemplates.jl/" target="_blank" rel="noopener noreferrer">here</a> (opens in a new tab).
 ```
 
 
@@ -113,6 +113,12 @@ This is a partial list of JuDoc's features that you may find interesting/useful;
   * minified output (via [`css-html-js-minify`](https://github.com/juancarlospaco/css-html-js-minify), requires `python3`)
 * ([docs](man/workflow/#(git)-synchronisation-1)) all-in-one "publish" step to compile, optimise and push your website
 
+There are a few features to deal with code:
+
+* ([docs](man/syntax/#Insertions-1)) insertion of code blocks with or without live-evaluation (a bit like [Weave.jl](https://github.com/mpastell/Weave.jl)),
+* ([docs](man/workflow/#Using-Literate.jl-1)) working with [Literate.jl](https://github.com/fredrikekre/Literate.jl)
+
+
 ### Why?
 
 There is a multitude of [static site generators](https://www.staticgen.com/) out there so why bother with yet another one? and is this one worth your time?
diff --git a/docs/src/lib/design.md b/docs/src/lib/design.md
index be6d22c1..aeaa464f 100644
--- a/docs/src/lib/design.md
+++ b/docs/src/lib/design.md
@@ -8,27 +8,27 @@ This page aims to shed some light on how JuDoc works and how the code is structu
 
 The overarching sequence for the initial _full pass_ is:
 
-1. retrieve the paths to files that should be processed (see [`JuDoc.jd_setup`](@ref)),
-1. process all files (see [`JuDoc.jd_fullpass`](@ref))
+1. retrieve the paths to files that should be processed ([`JuDoc.jd_setup`](@ref)),
+1. process all files ([`JuDoc.jd_fullpass`](@ref))
    * if it's a markdown file convert it to HTML (see below),
    * place generated file in appropriate locations.
 
-In general the user will use `serve()` which triggers a full pass followed by a loop that re-processes files individually upon modifications (see [`JuDoc.jd_loop`](@ref)).
+In general the user will use `serve()` which triggers a full pass followed by a loop that re-processes files individually upon modifications ([`JuDoc.jd_loop`](@ref)).
 
 ### File processing
 
-The file processing is controlled by the function [`JuDoc.process_file`](@ref) which, itself, is a thin wrapper around the function [`JuDoc.process_file_err`](@ref).
+The file processing is controlled by the function [`JuDoc.process_file`](@ref) which, itself, is a thin wrapper around the function [`JuDoc.process_file_err`](@ref) (the first one processes any errors that may be generated during the processing of files).
 There are three types of files:
 
 * markdown files (`.md`) which are parsed and converted into HTML,
-* HTML files (`.html`) which are parsed and re-generated after solving any templating commands there may be,
+* HTML files (`.html`) which are parsed and re-generated after solving any templating commands they may contain,
 * other files which are just copied over to the relevant location.
 
 In the case of markdown files, the function [`JuDoc.write_page`](@ref) is called and is formed of 3 key stages:
 
-1. parsing of the markdown (see [`JuDoc.convert_md`](@ref))
-1. conversion to HTML and assembly of different blocks into one page (see [`JuDoc.convert_html`](@ref) and [`JuDoc.build_page`](@ref)),
-1. writing the HTML in the appropriate location, possibly after pre-rendering javascript (see [`JuDoc.js_prerender_katex`](@ref), [`JuDoc.js_prerender_highlight`](@ref)).
+1. parsing of the markdown ([`JuDoc.convert_md`](@ref))
+1. conversion to HTML and assembly of different blocks into one page ([`JuDoc.convert_html`](@ref) and [`JuDoc.build_page`](@ref)),
+1. writing the HTML in the appropriate location, possibly after pre-rendering javascript ([`JuDoc.js_prerender_katex`](@ref), [`JuDoc.js_prerender_highlight`](@ref)).
 
 
 ## Parsing
@@ -75,7 +75,7 @@ julia> JuDoc.to(ocb[2])
 
 ### Finding Tokens
 
-The function [`JuDoc.find_tokens`](@ref) takes content, reads it from left to right and returns a list of `Token`.
+The function [`JuDoc.find_tokens`](@ref) takes content, reads it from left to right and returns a list of `Token`s.
 It takes a string, and two "token dictionaries".
 The first one describes tokens that span _multiple characters_ while the second one correspond to tokens that span _a single character_.
 
@@ -160,18 +160,18 @@ julia> ocb[1].ss
 
 ```
 
-When a token has been snapped up in a ocblock, it is marked as inactive.
+When a token has been snapped up in a ocblock, it is marked as inactive so that it doesn't get re-processed.
 
 ### LaTeX blocks
 
 When parsing a markdown file, after finding tokens and ocblocks, JuDoc tries to find [`JuDoc.LxDef`](@ref) and [`JuDoc.LxCom`](@ref) blocks (also `AbstractBlock`).
 
-The first one corresponds to LaTeX definitions of the form `\newcommand{\name}[narg]{def}` while the second one corresponds to LaTeX commands such as `\foo` or `\foo{bar}` or `\foo{bar}{baz}` etc.
+The first one corresponds to LaTeX _definitions_ of the form `\newcommand{\name}[narg]{def}` while the second one corresponds to LaTeX _commands_ such as `\foo` or `\foo{bar}` or `\foo{bar}{baz}` etc.
 
 The function [`JuDoc.find_md_lxdefs`](@ref) takes a vector of active tokens and a ocblocks and finds sequences that match the format of a newcommand.
 Its counterpart, [`JuDoc.find_md_lxcoms`](@ref) takes a vector of active tokens and definitions and ocblocks and finds sequences that match the format of a command.
 
-```julia
+```julia-repl
 julia> s = raw"a \newcommand{\foo}[1]{_blah #1_} and \foo{hello} done.";
 julia> t = JuDoc.find_tokens(s, JuDoc.MD_TOKENS, JuDoc.MD_1C_TOKENS);
 julia> ocb, t = JuDoc.find_all_ocblocks(t, JuDoc.MD_OCB);
@@ -210,9 +210,33 @@ These blocks can then be assembled in larger blocks such as `HCond` (correspondi
 
 ## Conversion
 
-TBD
-
 ### Markdown conversion
 
+The core function corresponding to the conversion of a markdown document is, as mentioned before, [`JuDoc.convert_md`](@ref).
+The first part corresponds to the parsing discussed above to find:
+
+1. tokens
+1. open/close blocks
+1. LaTeX-like definitions
+1. LaTeX-like commands
+1. Markdown definitions (e.g. `@def x = 5`)
+
+This parsing step creates a number of _blocks_ which each have to be processed in turn.
+The function [`JuDoc.form_inter_md`](@ref) takes the vector of all blocks and latex definitions and forms an intermediate markdown where places where an insertion must occur are marked with `##JDINSERT##`.
+This intermediate markdown is then fed to the function [`JuDoc.md2html`](@ref) which wraps around Julia's Markdown to HTML.
+
+Finally the function [`JuDoc.convert_inter_html`](@ref) takes the partial markdown and inserts the appropriately processed blocks where the `##JDINSERT##` are.
+The function [`JuDoc.convert_block`](@ref) takes care of how blocks are converted before insertion (for instance how a math block should be properly fenced).
 
 ### HTML conversion
+
+The core function corresponding to the conversion of a html document is, as mentioned before, [`JuDoc.convert_html`](@ref).
+The first part corresponds to the parsing discussed above to find:
+
+1. tokens
+1. open/close blocks
+1. conditional blocks (`if ... elseif ... else ... end`)
+1. conditional def blocks (`isdef ... end`)
+1. conditional page blocks (`ifpage ... end`)
+
+The final HTML page is written by sequentially writing what's between the blocks and then replacing the blocks by the appropriate content using the function [`JuDoc.convert_hblock`](@ref).
diff --git a/docs/src/lib/internals.md b/docs/src/lib/internals.md
index a4383857..0a4892ad 100644
--- a/docs/src/lib/internals.md
+++ b/docs/src/lib/internals.md
@@ -17,7 +17,12 @@ JuDoc.process_file_err
 ```@docs
 JuDoc.write_page
 JuDoc.convert_md
+JuDoc.form_inter_md
+JuDoc.md2html
+JuDoc.convert_inter_html
+JuDoc.convert_block
 JuDoc.convert_html
+JuDoc.convert_hblock
 JuDoc.build_page
 ```
 
diff --git a/docs/src/man/syntax.md b/docs/src/man/syntax.md
index e5f88d8a..c99d2791 100644
--- a/docs/src/man/syntax.md
+++ b/docs/src/man/syntax.md
@@ -18,7 +18,7 @@ Most of what is presented here is also shown in that example.
   * [Whitespaces](#Whitespaces-1)
   * [Local vs global](#Local-vs-global-1)
   * [Hyper-references](#Hyper-references-1)
-* [Insertions](#Insertions-1)
+* [Code insertions](#Code-insertions-1)
   * [Folder structure](#Folder-structure-1)
   * [Code](#Code-1)
   * [Plain-text output](#Plain-text-output-1)
@@ -331,46 +331,129 @@ As in LaTeX, if the reference is undefined, the command will be replaced by **(?
     In the future, there may be a possibility to define specific bibliography styles.
     I've not yet come around to it but feel free to open an issue if you would like this or would like to suggest a way to do it.
 
-## Insertions
+## Code insertions
 
 Sometimes, when presenting code in a post, you would like to make sure the code works and it can be annoying to have to copy-paste it around then copy its output, especially if you decide to make modifications on the way in which case you have to repeat the process.
-For this reason, insertions can be convenient. The philosophy is:
 
-* keep your code snippets in `assets/scripts` (or subfolders) where they can be run and their output can be saved, this can be compared to a `test/` folder in a Julia package,
-* run some or all of the snippets,
-* use `\input{...}{...}` in your markdown (see below) and when the website is updated, it will plug-in the most recent parts that have been generated.
+In JuDoc there are two ways to do this.
 
-That way, if you modify the code, everything will be updated on the website too while ensuring that the code actually runs and generates the output you're displaying.
+1. For Julia, a live-evaluation of code blocks is supported not unlike the [Weave.jl](https://github.com/mpastell/Weave.jl) package,
+1. For all language, you can run the script separately and insert the code and/or the output of the code in the page.
+
+### On-the-fly evaluation
+
+!!! note
+
+    **Evaluation time**: it is important to realise that when the code block is created or modified and the page is saved, it will trigger a page compilation that will _wait_ for the evaluation of the code block to complete. So if your code block takes a long time to execute, the page will not be updated before that's done.
+    That being said, if you don't modify the code block, it will only be executed _once_ as the output is saved to file.
 
 !!! note
 
-    JuDoc is not meant to be a competitor to [Weave.jl](https://github.com/mpastell/Weave.jl) and
-    consequently does not run your code. This ensures that application of page modifications is not
-    massively slowed down by the execution of some code that appears in it (and would potentially
-    be executed every time you modify the page).
-    It is very much meant to be a two way process, one where you update/modify the code and one
-    where you compile the website which plugs in the relevant, updated, parts that have been produced.
+    **Sandboxing**: on-the-fly evaluation of code blocks is still a bit experimental. Among other things, the code is _not sandboxed_ which means that if you have two code blocks one after the other, the second one has access to what's defined in the first. This is natural within the same page, but it also works _across_ pages. However it would be really bad practice to rely on this as the order in which pages are compiled is not always the same.
+    In short: take a page as one Julia notebook and pay attention to all your variables and functions to be defined on that page.
+
+Code blocks that should not be evaluated should be added without anything special so for instance:
+
+`````judoc
+```julia
+a = 10
+```
+`````
+
+Code blocks that should be evaluated should be added with `julia:path/to/script` where `path/to/script` indicates _where_ the script corresponding to the code block will be saved.
+
+`````judoc
+```julia:./code/ex1
+a = 10
+@show a
+```
+`````
+
+What this will do is:
 
-### Folder structure
+1. write the code to the file `/assets/[subpath]/code/ex1.jl`
+1. run the code and capture its output (`STDOUT`) and write it to `/assets/[subpath]/code/output/ex1.out`
 
-The folder structure for `assets/scripts` should resemble
+The `[subpath]` here is the _exact same sub-path structure_ than to the page where the code block is inserted.
+So to clarify, let's say you wrote the above code-block in
 
 ```
-.
-└──scripts
-    ├── generate_results.jl
-    ├── output              # generated
-    │   ├── script1.txt
-    │   └── script2.png
-    ├── script1.jl
-    └── script2.jl
+/src/pages/folder1/page1.md
 ```
 
-Your scripts would be `script1.jl` and `script2.jl` (these can be in subfolders of `scripts/` as well).
+then with the syntax above, the script will be saved in
+
+```
+/assets/pages/folder1/code/ex1.jl
+```
+
+#### More on paths
+
+There are three ways you can specify where the script corresponding to a code-block should be saved.
+
+1. `./[p]/script` is as above, it will write the code block to `/assets/[subpath]/p/script.jl` where `subpath` corresponds to the sub-path of the page where the code block is inserted (path below `/src/`)
+1. `p/script` will write the code block to `/assets/p/script.jl`
+1. `/p/script` will write the code block to `/p/script.jl`
+
+**Note**: when code blocks are evaluated and their output (`STDOUT`) is captured, it saved at `[path]/output/script.out` where `[path]` is what precedes `script.jl` in the cases above.
+
+#### Inserting the output
+
+Ok so let's say you've added the following code block:
+
+`````judoc
+```julia:./code_pg1/ex1
+using LinearAlgebra
+a = [1, 2, 3]
+@show dot(a, a)
+```
+`````
+
+In order to show the output, just write
+
+```judoc
+\output{./code_pg1/ex1}
+```
+
+which in the present example will introduce exactly the following HTML
+
+```html
+<pre><code>dot(a, a) = 14</code></pre>
+```
+
+which will look like
+
+```
+dot(a, a) = 14
+```
+
+If you now change the vector `a` in the code block, the page will be re-compiled with the code-block re-evaluated and the new output will be shown.
+
+#### Hiding lines
+
+Sometimes you may want to run some lines but hide them from the presentation, for this just use `# hide` at the end of the line:
+
+`````judoc
+```julia:./code_pg1/ex1
+using LinearAlgebra # hide
+a = [1, 2, 3]
+@show dot(a, a)
+```
+`````
+
+### Separate evaluation
+
+The philosophy here is:
+
+* keep your code snippets in appropriate subfolders of `/assets/` where they can be run and their output can be saved, this can be compared to a `test/` folder in a Julia package,
+* run some or all of the snippets,
+* use `\input{...}{...}` in your markdown (see below) and when the website is updated, it will plug-in the most recent parts that have been generated.
+
+That way, if you modify the code, everything will be updated on the website too while ensuring that the code actually runs and generates the output you're displaying.
 
-The script files can contain `# hide` at the end of lines you do not want to show (`hide` is not case sensitive so `# HiDe` would be fine too).
+Again, the script files can contain `# hide` at the end of lines you do not want to show (`hide` is not case sensitive so `# HiDe` would be fine too).
 
-The `generate_results.jl` file should run the scripts and redirect outputs to the `assets/scripts/output` directory.
+The `generate_results.jl` file should run the scripts and redirect outputs to the `assets/[path]/output` directory.
 You can use something like the script below (if you generate an example website with [`newsite`](@ref), it's already in there) though you can of course modify it as you wish.
 
 ```julia
@@ -381,26 +464,26 @@ dir = @__DIR__
 Small helper function to run some code and redirect the output (stdout) to a file.
 """
 function genplain(s::String)
-    open(joinpath(dir, "output", "$(splitext(s)[1]).txt"), "w") do outf
+    open(joinpath(dir, "output", "$(splitext(s)[1]).out"), "w") do outf
         redirect_stdout(outf) do
             include(joinpath(dir, s))
         end
     end
 end
-# run `script1.jl` and redirect what it prints to `output/script1.txt`
+# run `script1.jl` and redirect what it prints to `output/script1.out`
 genplain("script1.jl")
 # run `script2.jl` which has a savefig(joinpath(@__DIR__, "output", "script2.png"))
 include("script2.jl")
 ```
 
-The function `genplain("scriptname.jl")` just redirects the output of the script to `output/scriptname.txt`.
+The function `genplain("scriptname.jl")` just redirects the output of the script to `output/scriptname.out`.
 So for instance if you have in `assets/scripts/script1.jl`
 
 ```julia
 print("hello")
 ```
 
-Then `genplain("script1.jl")` will generate `assets/scripts/output/script1.txt` with content
+Then `genplain("script1.jl")` will generate `assets/scripts/output/script1.out` with content
 
 ```julia
 hello
@@ -410,40 +493,40 @@ hello
 
     You could have scripts in any language here (`R`, `Python`, ...) as long as the folder structure is the same.
 
-### Code
+#### Inserting code
 
 In order to insert the code of a script and have it highlighted you can use
 
 ```judoc
-\input{julia}{script1.jl}
+\input{julia}{scripts/script1.jl}
 ```
 
-or `\input{code:julia}{script1.jl}`. This will insert the content of the file `assets/scripts/script1.jl` into a block that will be highlighted as julia code.
+or `\input{code:julia}{scripts/script1.jl}`. This will insert the content of the file `assets/scripts/script1.jl` (see also the section earlier on paths) into a block that will be highlighted as julia code.
 
 ### Plain-text output
 
 In order to insert the plain-text output of a script, you can use
 
 ```judoc
-\input{output}{script1.jl}
+\input{output}{scripts/script1.jl}
 ```
 
-or `\input{output:plain}{script1.jl}`. This will insert the content of the file `assets/scripts/script1.txt` into a non-highlighted code-block.
+or `\input{output:plain}{scripts/script1.jl}`. This will insert the content of the file `assets/scripts/script1.out` into a non-highlighted code-block.
 
 ### Plot output
 
 In order to insert a plot generated by a script, you can use
 
 ```judoc
-\input{plot}{script1.jl}
+\input{plot}{scripts/script1.jl}
 ```
 
-or `\input{plot:id}{script1.jl}`. This will look for an image file with root name `assets/scripts/script1.ext` where `ext` is `gif, png, jp(e)g, svg`.
+or `\input{plot:id}{scripts/script1.jl}`. This will look for an image file with root name `assets/scripts/script1.ext` where `ext` is `gif, png, jp(e)g, svg`.
 If you use `plot:id` then it will look for an image file with root name `assets/scripts/script1id.ext`.
 
 The `plot:id` option is useful if you have a script that generates several plots for instance.
 
-### Slicing up
+#### Slicing up
 
 The structure in the `generate_results.jl` effectively means that all your code is run as one big script.
 This also means that if you want to slice some of your code in several parts and show intermediate outputs (e.g. plots), well you can just do that by having a `script_1_p1.jl`, `script_1_p2.jl` etc. and then just use  `\input` multiple times.
diff --git a/docs/src/man/workflow.md b/docs/src/man/workflow.md
index f545c50f..e580ae7f 100644
--- a/docs/src/man/workflow.md
+++ b/docs/src/man/workflow.md
@@ -102,7 +102,7 @@ The layout can also depend on the page you're on if it uses `{{ispage path/to/pa
 
 **CSS**
 
-Unsurprisingly, the style sheets in `_css/` will help you tune the way your site looks.
+The style sheets in `_css/` will help you tune the way your site looks.
 The `judoc.css` is the stylesheet that corresponds more specifically to the styling of the `.jd-content` div and all that goes in it, it is usually the first style-sheet that should be loaded.
 The simplest way to adjust the style easily would be to define your own stylesheet `_css/adjustments.css` and it be the last stylesheet loaded in `_html_parts/head.html` so that you can easily overwrite whatever properties you don't like and define your own.
 You could also have specific stylesheet that would only be loaded on specific pages using `{{ispages ...}}` (see [Templating](@ref)).
@@ -159,7 +159,7 @@ Assuming you used the [`newsite`](@ref) function to get started, you have the `l
 ```
 
 If you want to change either how things look or which languages are supported, you should head to [highlightjs.org](https://highlightjs.org/download/), select the languages you want in the **Custom package** section, download the bundle and copy over the relevant files to `libs/highlight/`.
-By default, `bash`, `html/xml`, `python`, `julia`, `julia-repl`, `css`, `r`, `markdown`, `ini/TOML`, `ruby` and `yaml` are supported.
+By default, the files that are created with [`newsite`](@ref) allow highlighting for `bash`, `html/xml`, `python`, `julia`, `julia-repl`, `css`, `r`, `markdown`, `ini/TOML`, `ruby` and `yaml`.
 
 Just remember to refer to the appropriate style-sheet in your HTML building blocks for instance the default `src/_html_parts/head_highlight.html` contains:
 
@@ -182,6 +182,14 @@ Once the repository is created, clone it on your computer, remove whatever is in
 
 Now just do the usual `git add`, `commit` and `push` and your site will be live in a matter of minutes.
 
+### Hosting the website as a project website
+
+You may want to host your website not as a user website on `username.github.io/` but as a project website on `username.github.io/project/`.
+
+```@raw html
+<span style="color:red;">TBD</span>, see <a href="https://github.com/tlienart/JuDoc.jl/issues/162">issue #162</a>.
+```
+
 ## Optimisation step
 
 The [`optimize`](@ref) function should typically be run before you push your website online.
diff --git a/src/JuDoc.jl b/src/JuDoc.jl
index 99a1235c..cf467ef9 100644
--- a/src/JuDoc.jl
+++ b/src/JuDoc.jl
@@ -10,13 +10,20 @@ import LiveServer
 
 using DocStringExtensions: SIGNATURES, TYPEDEF
 
+export serve, publish, cleanpull, newsite, optimize
+
+# -----------------------------------------------------------------------------
+#
+# CONSTANTS
+#
+
+"""Big number when we want things to be far."""
 const BIG_INT = typemax(Int)
-const JD_SERVE_FIRSTCALL = Ref(true)
 
+"""Flag for debug mode."""
 const JD_DEBUG = Ref(false)
 
-export serve, publish, cleanpull, newsite, optimize
-
+"""Dict to keep track of languages and how comments are indicated."""
 const HIGHLIGHT = Dict{String,Pair{String,Any}}(
     "fortran"    => "!" => Lexers.FortranLexer,
     "julia-repl" => "#" => Lexers.JuliaConsoleLexer,
@@ -25,11 +32,23 @@ const HIGHLIGHT = Dict{String,Pair{String,Any}}(
     "r"          => "#" => Lexers.RLexer,
     "toml"       => "#" => Lexers.TOMLLexer)
 
+"""Dict to keep track of languages and their extensions."""
+const LANG_EXT = Dict{String,String}(
+    "julia"   => ".jl",
+    "python"  => ".py",
+    "r"       => ".r",
+    "fortran" => ".f90",
+    "matlab"  => ".m")
+
+"""Path to the JuDoc repo."""
 const JUDOC_PATH = splitdir(pathof(JuDoc))[1] # .../JuDoc/src
+
+"""Path to some temporary folder that will be used by JuDoc."""
 const TEMPL_PATH = joinpath(JUDOC_PATH, "templates")
 
 # copied from Base/path.jl
 if Sys.isunix()
+    """Indicator for directory separation on the OS."""
     const PATH_SEP = "/"
 elseif Sys.iswindows()
     const PATH_SEP = "\\"
@@ -37,13 +56,14 @@ else
     error("Unhandled OS")
 end
 
-"""
-    JD_VAR_TYPE
-
-Type of the containers for page variables.
-"""
+"""Type of the containers for page variables."""
 const JD_VAR_TYPE = Dict{String,Pair{K,NTuple{N, DataType}} where {K, N}}
 
+"""Relative path to the current file being processed."""
+const JD_CURPATH = Ref("")
+
+# -----------------------------------------------------------------------------
+
 include("build.jl") # check if user has Node/minify
 
 # PARSING
diff --git a/src/converter/html.jl b/src/converter/html.jl
index adeeeafa..2e79a23e 100644
--- a/src/converter/html.jl
+++ b/src/converter/html.jl
@@ -15,7 +15,7 @@ function convert_html(hs::AbstractString, allvars::JD_VAR_TYPE, fpath::AbstractS
     qblocks = qualify_html_hblocks(hblocks)
     # Find overall conditional blocks (if ... elseif ... else ...  end)
     cblocks, qblocks = find_html_cblocks(qblocks)
-    # Find conditional def blocks (ifdef / ifndef)
+    # Find conditional def blocks (isdef / isnotdef)
     cdblocks, qblocks = find_html_cdblocks(qblocks)
     # Find conditional page blocks (ispage / isnotpage)
     cpblocks, qblocks = find_html_cpblocks(qblocks)
diff --git a/src/converter/lx.jl b/src/converter/lx.jl
index 5fb43a87..1cb64905 100644
--- a/src/converter/lx.jl
+++ b/src/converter/lx.jl
@@ -13,6 +13,7 @@ function resolve_lxcom(lxc::LxCom, lxdefs::Vector{LxDef}; inmath::Bool=false)::S
     # sort special commands where the input depends on context (see hyperrefs and inputs)
     haskey(JD_REF_COMS, name) && return JD_REF_COMS[name](lxc)
     (name == "\\input")       && return resolve_input(lxc)
+    (name == "\\output")      && return resolve_output(lxc)
 
     # retrieve the definition attached to the command
     lxdef = getdef(lxc)
@@ -86,13 +87,21 @@ Reset the JD_LOC_BIBREFDICT dictionary.
 def_JD_LOC_BIBREFDICT!() = (empty!(JD_LOC_BIBREFDICT); nothing)
 
 
+"""
+$(SIGNATURES)
+
+Given a `label` command, replace it with an html anchor.
+"""
+add_label(λ::LxCom)::String = "<a id=\"$(refstring(strip(content(λ.braces[1]))))\"></a>"
+
+
 """
 $(SIGNATURES)
 
 Given a `biblabel` command, update `JD_LOC_BIBREFDICT` to keep track of the reference so that it
 can be linked with a hyperreference.
 """
-function form_biblabel(λ::LxCom)::String
+function add_biblabel(λ::LxCom)::String
     name = refstring(strip(content(λ.braces[1])))
     JD_LOC_BIBREFDICT[name] = content(λ.braces[2])
     return "<a id=\"$name\"></a>"
@@ -132,36 +141,61 @@ const JD_REF_COMS = Dict{String, Function}(
     "\\cite"     => (λ -> form_href(λ, "BIBR"; parens=""=>"", class="bibref")),
     "\\citet"    => (λ -> form_href(λ, "BIBR"; parens=""=>"", class="bibref")),
     "\\citep"    => (λ -> form_href(λ, "BIBR"; class="bibref")),
-    "\\biblabel" => form_biblabel,
+    "\\biblabel" => add_biblabel,
+    "\\label"    => add_label,
     )
 
 
 """
 $(SIGNATURES)
 
-Internal function to check that a given filename exists in `JD_PATH[:scripts]`. See also
-[`resolve_input`](@ref).
+Internal function to resolve a relative path to `/assets/` and return the resolved dir as well
+as the file name; it also checks that the dir exists. It returns the full path to the file, the
+path of the directory containing the file, and the name of the file without extension.
+See also [`resolve_input`](@ref).
 """
-function check_input_fname(fname::AbstractString)::String
-    fp = joinpath(JD_PATHS[:scripts], fname)
-    isfile(fp) || throw(ArgumentError("Couldn't find file $fp when trying to resolve an \\input."))
-    return fp
+function check_input_rpath(rpath::AbstractString, lang::AbstractString="")::NTuple{3,String}
+    fpath = resolve_assets_rpath(rpath)
+    if isempty(splitext(fpath)[2])
+        ext = get(LANG_EXT, lang) do
+            ".xx"
+        end
+        fpath *= ext
+    end
+    dir, fname = splitdir(fpath)
+
+    # see fill_extension, this is the case where nothing came up
+    if endswith(fname, ".xx")
+        # try to find a file with whatever extension otherwise throw an error
+        files = readdir(dir)
+        fn = splitext(fname)[1]
+        k = findfirst(e -> splitext(e)[1] == fn, files)
+        k === nothing && throw(ArgumentError("Couldn't find a relevant file when trying to " *
+                                             "resolve an \\input command. (given: $rpath)"))
+        fname = files[k]
+        fpath = joinpath(dir, fname)
+    elseif !isfile(fpath)
+        throw(ArgumentError("Couldn't find a relevant file when trying to resolve an \\input " *
+                            "command. (given: $rpath)"))
+    end
+    return fpath, dir, splitext(fname)[1]
 end
 
+
 """
 $(SIGNATURES)
 
-Internal function to read the content of a script file and highlight it using `Highlights.jl`.
-See also [`resolve_input`](@ref).
+Internal function to read the content of a script file and highlight it using either highlight.js
+or `Highlights.jl`. See also [`resolve_input`](@ref).
 """
-function resolve_input_hlcode(fname::AbstractString, lang::AbstractString;
-                              use_hl::Bool=true)::String
-    fp = check_input_fname(fname)
+function resolve_input_hlcode(rpath::AbstractString, lang::AbstractString;
+                              use_hl::Bool=false)::String
+    fpath, _, _ = check_input_rpath(rpath)
     # Read the file while ignoring lines that are flagged with something like `# HIDE`
     comsym, lexer = HIGHLIGHT[lang]
     hide = Regex("\\s$(comsym)(\\s)*?(?i)hide")
     io_in = IOBuffer()
-    open(fp, "r") do f
+    open(fpath, "r") do f
         for line ∈ readlines(f)
              # - if there is a \s#\s+HIDE , skip that line
              match(hide, line) === nothing || continue
@@ -169,136 +203,146 @@ function resolve_input_hlcode(fname::AbstractString, lang::AbstractString;
              write(io_in, "\n")
         end
     end
+    code = String(take!(io_in))
+    endswith(code, "\n") && (code = chop(code, tail=1))
     if use_hl
         io_out = IOBuffer()
-        highlight(io_out, MIME("text/html"), String(take!(io_in)), lexer)
+        highlight(io_out, MIME("text/html"), code, lexer)
         return String(take!(io_out))
     end
-    return "<pre><code class=\"language-$lang\">$(String(take!(io_in)))</code></pre>"
+    return "<pre><code class=\"language-$lang\">$code</code></pre>"
 end
 
+
 """
 $(SIGNATURES)
 
 Internal function to read the content of a script file and highlight it using `highlight.js`. See
 also [`resolve_input`](@ref).
 """
-function resolve_input_othercode(fname::AbstractString, lang::AbstractString)::String
-    fp = check_input_fname(fname)
-    return "<pre><code class=\"language-$lang\">$(read(fp, String))</code></pre>"
+function resolve_input_othercode(rpath::AbstractString, lang::AbstractString)::String
+    fpath, _, _ = check_input_rpath(rpath)
+    return "<pre><code class=\"language-$lang\">$(read(fpath, String))</code></pre>"
 end
 
+
 """
 $(SIGNATURES)
 
 Internal function to read the raw output of the execution of a file and display it in a pre block.
 See also [`resolve_input`](@ref).
 """
-function resolve_input_plainoutput(fname::AbstractString)::String
-    # will throw an error if fname doesn't exist as a script
-    check_input_fname(fname)
-    # find a file in output that has the same root name
-    d, fn = splitdir(fname)
-    fn, _ = splitext(fn)
-    outdir = joinpath(JD_PATHS[:scripts], "output", d)
-    isdir(outdir) || throw(ErrorException("I found an input command but not $outdir."))
-    fp = ""
-    for (root, _, files) ∈ walkdir(outdir)
-        for (f, e) ∈ splitext.(files)
-            if fn == f
-                fp = joinpath(root, f * e)
-                break
-            end
-        end
-        fp == "" || break
-    end
-    fp != "" || throw(ErrorException("I found an input command but not a relevant output file."))
-    return "<pre><code>$(read(fp, String))</code></pre>"
+function resolve_input_plainoutput(rpath::AbstractString)::String
+    # will throw an error if rpath doesn't exist
+    _, dir, fname = check_input_rpath(rpath)
+    out_file = joinpath(dir, "output", fname * ".out")
+    # check if the output file exists
+    isfile(out_file) || throw(ErrorException("I found an \\input but no relevant output file."))
+    # return the content in a pre block
+    return "<pre><code>$(read(out_file, String))</code></pre>"
 end
 
+
 """
 $(SIGNATURES)
 
-Internal function to read a plot outputted by script `fname`, possibly named with `id`. See also
+Internal function to read a plot outputted by script `rpath`, possibly named with `id`. See also
 [`resolve_input`](@ref).
 """
-function resolve_input_plotoutput(fname::AbstractString, id::AbstractString="")::String
-    fp = check_input_fname(fname)
-    # find an img in output that has the same root name
-    d, fn = splitdir(fname)
-    fn, _ = splitext(fn)
-    fn *= id
-    outdir = joinpath(JD_PATHS[:scripts], "output", d)
-    isdir(outdir) || throw(ErrorException("I found an input command but not $outdir."))
-    fp = ""
-    for (root, _, files) ∈ walkdir(outdir)
+function resolve_input_plotoutput(rpath::AbstractString, id::AbstractString="")::String
+    # will throw an error if rpath doesn't exist
+    _, dir, fname = check_input_rpath(rpath)
+    plt_name = fname * id
+    # relative dir /assets/...
+    reldir = normpath(joinpath("/assets/", dirname(rpath)))
+    # find a plt in output that has the same root name
+    out_path = joinpath(dir, "output")
+    isdir(out_path) || throw(ErrorException("I found an input plot but not output dir."))
+    out_file = ""
+    for (root, _, files) ∈ walkdir(out_path)
         for (f, e) ∈ splitext.(files)
             lc_e = lowercase(e)
-            if fn == f && lc_e ∈ (".gif", ".jpg", ".jpeg", ".png", ".svg")
-                fp = f * lc_e
+            if f == plt_name && lc_e ∈ (".gif", ".jpg", ".jpeg", ".png", ".svg")
+                out_file = joinpath(reldir, "output", plt_name * lc_e)
                 break
             end
         end
-        fp == "" || break
+        out_file == "" || break
     end
-    fp != "" || throw(ErrorException("I found an input command but not a relevant output plot."))
-    return "<img src=\"/assets/scripts/output/$(joinpath(d, fp))\" id=\"judoc-out-plot\"/>"
+    # error if no file found
+    out_file != "" || throw(ErrorException("I found an input plot but no relevant output plot."))
+    # wrap it in img block
+    return "<img src=\"$(out_file)\" id=\"judoc-out-plot\"/>"
 end
 
+
 """
 $(SIGNATURES)
 
 Resolve a command of the form `\\input{code:julia}{path_to_script}` where `path_to_script` is a
-valid subpath of `/scripts/`. All paths should be expressed relatively to `scripts/` so for
-instance `script1.jl` or `folder1/script1.jl`.
+valid subpath of `/assets/`. All paths should be expressed relatively to `/assets/` i.e.
+`/assets/[subpath]/script.jl`. If things are not in `assets/`, start the path
+with `../` to go up one level then go down for instance `../src/pages/script1.jl`.
+Forward slashes should also be used on windows.
 
 Different actions can be taken based on the first bracket:
 1. `{julia}` or `{code:julia}` will insert the code in the script with appropriate highlighting
 2. `{output}` or `{output:plain}` will look for an output file in `/scripts/output/path_to_script` and will just print the content in a non-highlighted code block
 3. `{plot}` or `{plot:id}` will look for a displayable image file (gif, png, jp(e)g or svg) in
-`/scripts/output/path_to_script` and will add an `img` block as a result. If an `id` is specified,
+`/assets/[subpath]/script1` and will add an `img` block as a result. If an `id` is specified,
 it will try to find an image with the same root name ending with `id.ext` where `id` can help
 identify a specific image if several are generated by the script, typically a number will be used.
 """
 function resolve_input(lxc::LxCom)::String
-    isdir(JD_PATHS[:scripts]) || throw(ExceptionError("I found an \\input command but the " *
-                                        "folder `scripts/` doesn't exist. Scripts corresponding " *
-                                        "to \\input commands must be in this folder."))
-    qual  = lowercase(strip(content(lxc.braces[1]))) # `code:julia`
-    fname = strip(content(lxc.braces[2])) # `scripts/script1.jl`
-
-    if occursin(":", qual)
-        p1, p2 = split(qual, ":")
+    qualifier = lowercase(strip(content(lxc.braces[1])))  # `code:julia`
+    rpath = strip(content(lxc.braces[2])) # [assets]/subpath/script{.jl}
+
+    if occursin(":", qualifier)
+        p1, p2 = split(qualifier, ":")
+        # code:julia
         if p1 == "code"
             if p2 ∈ ("julia", "fortran", "julia-repl", "matlab", "r", "toml")
-                return resolve_input_hlcode(fname, p2; use_hl=false)
+                return resolve_input_hlcode(rpath, p2; use_hl=false)
             else # another language descriptor, let the user do that with highlights.js
-                return resolve_input_othercode(fname, qual)
+                return resolve_input_othercode(rpath, qualifier)
             end
+        # output:plain
         elseif p1 == "output"
             if p2 == "plain"
-                return resolve_input_plainoutput(fname)
+                return resolve_input_plainoutput(rpath)
             # elseif p2 == "table"
-            #     return resolve_input_tableoutput(fname)
+            #     return resolve_input_tableoutput(rpath)
             else
-                throw(ArgumentError("I found an \\input command but couldn't interpret \"$qual\"."))
+                throw(ArgumentError("I found an \\input but couldn't interpret \"$qualifier\"."))
             end
+        # plot:id
         elseif p1 == "plot"
-            return resolve_input_plotoutput(fname, p2)
+            return resolve_input_plotoutput(rpath, p2)
         else
-            throw(ArgumentError("I found an \\input command but couldn't interpret \"$qual\"."))
+            throw(ArgumentError("I found an \\input but couldn't interpret \"$qualifier\"."))
         end
     else
-        if qual == "output"
-            return resolve_input_plainoutput(fname)
-        elseif qual == "plot"
-            return resolve_input_plotoutput(fname)
-        # elseif qual == "table"
-        #     return resolve_input_tableoutput(fname)
-        elseif qual ∈ ("julia", "fortran", "julia-repl", "matlab", "r", "toml")
-            return resolve_input_hlcode(fname, qual; use_hl=false)
+        if qualifier == "output"
+            return resolve_input_plainoutput(rpath)
+        elseif qualifier == "plot"
+            return resolve_input_plotoutput(rpath)
+        # elseif qualifier == "table"
+        #     return resolve_input_tableoutput(rpath)
+        elseif qualifier ∈ ("julia", "fortran", "julia-repl", "matlab", "r", "toml")
+            return resolve_input_hlcode(rpath, qualifier; use_hl=false)
         else # assume it's another language descriptor, let the user do that with highlights.js
-            return resolve_input_othercode(fname, qual)
+            return resolve_input_othercode(rpath, qualifier)
         end
     end
 end
+
+
+"""
+$SIGNATURES
+
+Internal function to resolve a `\\output{rpath}` (finds the output and shows it).
+"""
+function resolve_output(lxc::LxCom)::String
+    rpath = strip(content(lxc.braces[1])) # [assets]/subpath/script{.jl}
+    return resolve_input_plainoutput(rpath)
+end
diff --git a/src/converter/md_blocks.jl b/src/converter/md_blocks.jl
index 3753dbba..f55c6e4b 100644
--- a/src/converter/md_blocks.jl
+++ b/src/converter/md_blocks.jl
@@ -95,8 +95,52 @@ $(SIGNATURES)
 Helper function for the code block case of `convert_block`.
 """
 function convert_code_block(ss::SubString)::String
-    m = match(r"```([a-z-]*)\s*\n?((?:.|\n)*)```", ss)
-    lang = m.captures[1]
-    code = m.captures[2]
-    return "<pre><code class=$lang>$code</code></pre>"
+    m = match(r"```([a-z-]*)(\:[a-zA-Z\\\/-_\.]+)?\s*\n?((?:.|\n)*)```", ss)
+    lang  = m.captures[1]
+    rpath = m.captures[2]
+    code  = m.captures[3]
+
+    if isnothing(rpath)
+        return "<pre><code class=\"language-$lang\">$code</code></pre>"
+    end
+    if lang!="julia"
+        @warn "Eval of non-julia code blocks is not supported at the moment"
+        return "<pre><code class=\"language-$lang\">$code</code></pre>"
+    end
+    # path currently has an indicative `:` we don't care about
+    rpath = rpath[2:end]
+
+    # Here we have a julia code block that was provided with a script path
+    # It will consequently be
+    #   1. written to script file unless it's already there
+    #   2. evaled (unless a file was there and output file is present), redirect out
+    #   3. inserted after scrapping out lines (see resolve_input)
+    path = resolve_assets_rpath(rpath)
+
+    endswith(path, ".jl") || (path *= ".jl")
+
+    out_path, fname = splitdir(path)
+    out_path = mkpath(joinpath(out_path, "output"))
+    out_name = splitext(fname)[1] * ".out"
+    out_path = joinpath(out_path, out_name)
+
+    # > 1.b check whether the file already exists and if so compare content
+    do_eval = !isfile(path) || read(path, String) != code || !isfile(out_path)
+    
+    if do_eval
+        write(path, code)
+        # step 2: execute the code while redirecting the output to file (note that
+        # everything is ran in one big sequence of scripts so in particular if there
+        # are 3 code blocks on the same page then the second and third one can use
+        # whatever is defined or loaded in the first (it also has access to scope of
+        # other pages but it's really not recommended to exploit that)
+        open(out_path, "w") do outf
+            redirect_stdout(outf)  do
+                Main.include(path)
+            end
+        end
+    end
+
+    # step 3, insertion of code stripping of "hide" lines.
+    return resolve_input_hlcode(rpath, "julia"; use_hl=false)
 end
diff --git a/src/jd_paths.jl b/src/jd_paths.jl
index eebc2ccd..421b904c 100644
--- a/src/jd_paths.jl
+++ b/src/jd_paths.jl
@@ -55,7 +55,6 @@ function set_paths!()::Dict{Symbol,String}
     JD_PATHS[:out_css]  = joinpath(JD_PATHS[:f],      "css")
     JD_PATHS[:libs]     = joinpath(JD_PATHS[:f],      "libs")
     JD_PATHS[:assets]   = joinpath(JD_PATHS[:f],      "assets")
-    JD_PATHS[:scripts]  = joinpath(JD_PATHS[:assets], "scripts")
 
     return JD_PATHS
 end
diff --git a/src/jd_vars.jl b/src/jd_vars.jl
index cce59692..9f8ef423 100644
--- a/src/jd_vars.jl
+++ b/src/jd_vars.jl
@@ -50,8 +50,9 @@ is processed.
     JD_LOC_VARS["hasmath"]  = Pair(true,    (Bool,))
     JD_LOC_VARS["hascode"]  = Pair(false,   (Bool,))
     JD_LOC_VARS["date"]     = Pair(Date(1), (String, Date, Nothing))
-    JD_LOC_VARS["jd_ctime"] = Pair(Date(1), (Date,))
-    JD_LOC_VARS["jd_mtime"] = Pair(Date(1), (Date,))
+    JD_LOC_VARS["jd_ctime"] = Pair(Date(1), (Date,))   # time of creation
+    JD_LOC_VARS["jd_mtime"] = Pair(Date(1), (Date,))   # time of last modification
+    JD_LOC_VARS["jd_rpath"] = Pair("",      (String,)) # local path to file src/[...]/blah.md
     return nothing
 end
 
@@ -78,9 +79,11 @@ the site. See [`resolve_lxcom`](@ref).
     JD_GLOB_LXDEFS["\\cite"]     = LxDef("\\cite",     1, SubString(""))
     JD_GLOB_LXDEFS["\\citet"]    = LxDef("\\citet",    1, SubString(""))
     JD_GLOB_LXDEFS["\\citep"]    = LxDef("\\citep",    1, SubString(""))
+    JD_GLOB_LXDEFS["\\label"]    = LxDef("\\label",    1, SubString(""))
     JD_GLOB_LXDEFS["\\biblabel"] = LxDef("\\biblabel", 2, SubString(""))
     # inclusion
     JD_GLOB_LXDEFS["\\input"]    = LxDef("\\input",    2, SubString(""))
+    JD_GLOB_LXDEFS["\\output"]   = LxDef("\\output",   1, SubString(""))
     return nothing
 end
 
diff --git a/src/manager/file_utils.jl b/src/manager/file_utils.jl
index 5373f279..fa1e875f 100644
--- a/src/manager/file_utils.jl
+++ b/src/manager/file_utils.jl
@@ -45,9 +45,15 @@ function write_page(root::String, file::String, head::String, pg_foot::String, f
     (content, jd_vars) = convert_md(read(fpath, String) * EOS, vJD_GLOB_LXDEFS)
 
     # adding document variables to the dictionary
+    # note that some won't change and so it's not necessary to do this every time
+    # but it takes negligible time to do this so ¯\_(ツ)_/¯ (and it's less annoying than
+    # to keep tabs on which file has already been treated etc).
     s = stat(fpath)
     set_var!(jd_vars, "jd_ctime", jd_date(unix2datetime(s.ctime)))
     set_var!(jd_vars, "jd_mtime", jd_date(unix2datetime(s.mtime)))
+    relpath = fpath[lastindex(JD_PATHS[:in])+2:end] # f1/blah/page1.md or index.md etc...
+    set_var!(jd_vars, "jd_rpath", relpath)
+    JD_CURPATH[] = relpath
 
     # 3. process blocks in the html infra elements based on `jd_vars`
     # (e.g.: add the date in the footer)
diff --git a/src/manager/judoc.jl b/src/manager/judoc.jl
index 9920a8fb..50c780be 100644
--- a/src/manager/judoc.jl
+++ b/src/manager/judoc.jl
@@ -114,9 +114,9 @@ function jd_fullpass(watched_files::NamedTuple; clear::Bool=false, verb::Bool=fa
     indexmd   = JD_PATHS[:in] => "index.md"
     indexhtml = JD_PATHS[:in] => "index.html"
 
-    # rest of the pages, done asynchronously
+    # rest of the pages
     s = 0
-    @sync begin
+    begin
         if isfile(joinpath(indexmd...))
             s += process_file(:md, indexmd, head, pg_foot, foot; clear=clear,
                               prerender=prerender)
@@ -133,7 +133,7 @@ function jd_fullpass(watched_files::NamedTuple; clear::Bool=false, verb::Bool=fa
                               prerender=prerender)
         end
     end
-    # return -1 if any task has failed
+    # return -1 if any page
     return ifelse(s<0, -1, 0)
 end
 
diff --git a/src/misc_utils.jl b/src/misc_utils.jl
index 74cb9c37..45a1ab95 100644
--- a/src/misc_utils.jl
+++ b/src/misc_utils.jl
@@ -135,3 +135,36 @@ function refstring(s::AbstractString)::String
     isempty(st) && return string(hash(s))
     return st
 end
+
+
+"""
+$(SIGNATURES)
+
+Internal function to take a unix path, split it along `/` and re-join it (which will lead to the
+same path on unix but not on windows). Only used in [`resolve_assets_rpath`](@ref).
+"""
+joinrp(rpath::AbstractString) = joinpath(split(rpath, '/')...)
+
+"""
+$(SIGNATURES)
+
+Internal function to resolve a relative path. See [`convert_code_block`](@ref) and
+[`resolve_input`](@ref).
+"""
+function resolve_assets_rpath(rpath::AbstractString)::String
+    @assert length(rpath) > 1 "relative path '$rpath' doesn't seem right"
+    if startswith(rpath, "/")
+        # this is a full path starting from the website root folder so for instance
+        # `/assets/blah/img1.png`
+        return normpath(joinpath(JD_PATHS[:f], joinrp(rpath[2:end])))
+    elseif startswith(rpath, "./")
+        # this is a path relative to the assets folder with the same path as the calling file so
+        # for instance if calling from `src/pages/pg1.md` with `./im1.png` it would refer to
+        # /assets/pages/im1.png
+        @assert length(rpath) > 2 "relative path '$rpath' doesn't seem right"
+        return normpath(joinpath(JD_PATHS[:assets], dirname(JD_CURPATH[]), joinrp(rpath[3:end])))
+    end
+    # this is a full path relative to the assets folder for instance `blah/img1.png` would
+    # correspond to `/assets/blah/img1.png`
+    return normpath(joinpath(JD_PATHS[:assets], joinrp(rpath)))
+end


Test patch:
diff --git a/test/_dummies/151.md b/test/_dummies/151.md
index 3e4c3c6d..6afc73d5 100644
--- a/test/_dummies/151.md
+++ b/test/_dummies/151.md
@@ -9,7 +9,7 @@ add OhMyREPL#master
 AAA
 
 ~~~
-<pre><code class="julia">"""
+<pre><code class="language-julia">"""
     bar(x[, y])
 
 BBB
@@ -29,7 +29,7 @@ For complex functions with multiple arguments use a argument list, also
 if there are many keyword arguments use `<keyword arguments>`:
 
 ~~~
-<pre><code class="julia">"""
+<pre><code class="language-julia">"""
     matdiag(diag, nr, nc; &ltkeyword arguments&gt)
 
 Create Matrix with number `vdiag` on the super- or subdiagonals and `vndiag`
diff --git a/test/converter/eval.jl b/test/converter/eval.jl
new file mode 100644
index 00000000..b16e1c1b
--- /dev/null
+++ b/test/converter/eval.jl
@@ -0,0 +1,142 @@
+function seval(st)
+    J.def_GLOB_VARS!()
+    J.def_GLOB_LXDEFS!()
+    m, _ = J.convert_md(st, collect(values(J.JD_GLOB_LXDEFS)))
+    h = J.convert_html(m, J.JD_VAR_TYPE())
+    return h
+end
+
+@testset "Eval code" begin
+    # see `converter/md_blocks:convert_code_block`
+    # see `converter/lx/resolve_input_*`
+    # --------------------------------------------
+    h = raw"""
+        Simple code:
+        ```julia:scripts/test1
+        a = 5
+        print(a^2)
+        ```
+        then:
+        \input{output}{scripts/test1}
+        done.
+        """ * J.EOS |> seval
+
+    spath = joinpath(J.JD_PATHS[:assets], "scripts", "test1.jl")
+    @test isfile(spath)
+    @test read(spath, String) == "a = 5\nprint(a^2)\n"
+
+    opath = joinpath(J.JD_PATHS[:assets], "scripts", "output", "test1.out")
+    @test isfile(opath)
+    @test read(opath, String) == "25"
+
+    @test occursin("code: <pre><code class=\"language-julia\">a = 5\nprint(a^2)</code></pre>", h)
+    @test occursin("then: <pre><code>25</code></pre> done.", h)
+end
+
+@testset "Eval code (errs)" begin
+    # see `converter/md_blocks:convert_code_block`
+    # --------------------------------------------
+    h = raw"""
+        Simple code:
+        ```python:scripts/testpy
+        a = 5
+        print(a**2)
+        ```
+        done.
+        """ * J.EOS |> seval
+
+    @test occursin("code: <pre><code class=\"language-python\">a = 5\nprint(a**2)\n</code></pre> done.", h)
+end
+
+@testset "Eval (rel-input)" begin
+    h = raw"""
+        Simple code:
+        ```julia:/scripts/test2
+        a = 5
+        print(a^2)
+        ```
+        then:
+        \input{output}{/scripts/test2}
+        done.
+        """ * J.EOS |> seval
+
+    spath = joinpath(J.JD_PATHS[:f], "scripts", "test2.jl")
+    @test isfile(spath)
+    @test read(spath, String) == "a = 5\nprint(a^2)\n"
+
+    opath = joinpath(J.JD_PATHS[:f], "scripts", "output", "test2.out")
+    @test isfile(opath)
+    @test read(opath, String) == "25"
+
+    @test occursin("code: <pre><code class=\"language-julia\">a = 5\nprint(a^2)</code></pre>", h)
+    @test occursin("then: <pre><code>25</code></pre> done.", h)
+
+    # ------------
+
+    J.JD_CURPATH[] = joinpath(J.JD_PATHS[:in], "pages", "pg1.md")[lastindex(J.JD_PATHS[:in])+2:end]
+
+    h = raw"""
+        Simple code:
+        ```julia:./code/test2
+        a = 5
+        print(a^2)
+        ```
+        then:
+        \input{output}{./code/test2}
+        done.
+        """ * J.EOS |> seval
+
+    spath = joinpath(J.JD_PATHS[:assets], "pages", "code", "test2.jl")
+    @test isfile(spath)
+    @test read(spath, String) == "a = 5\nprint(a^2)\n"
+
+    opath = joinpath(J.JD_PATHS[:assets], "pages", "code", "output" ,"test2.out")
+    @test isfile(opath)
+    @test read(opath, String) == "25"
+
+    @test occursin("code: <pre><code class=\"language-julia\">a = 5\nprint(a^2)</code></pre>", h)
+    @test occursin("then: <pre><code>25</code></pre> done.", h)
+end
+
+@testset "Eval code (module)" begin
+    h = raw"""
+        Simple code:
+        ```julia:scripts/test1
+        using LinearAlgebra
+        a = [5, 2, 3, 4]
+        print(dot(a, a))
+        ```
+        then:
+        \input{output}{scripts/test1}
+        done.
+        """ * J.EOS |> seval
+    # dot(a, a) == 54
+    @test occursin("then: <pre><code>54</code></pre> done.", h)
+end
+
+@testset "Eval code (img)" begin
+    h = raw"""
+        Simple code:
+        ```julia:scripts/test1
+        write(joinpath(@__DIR__, "output", "test1.png"), "blah")
+        ```
+        then:
+        \input{plot}{scripts/test1}
+        done.
+        """ * J.EOS |> seval
+    @test occursin("then: <img src=\"/assets/scripts/output/test1.png\" id=\"judoc-out-plot\"/> done.", h)
+end
+
+@testset "Eval code (exception)" begin
+    h = raw"""
+        Simple code:
+        ```julia:scripts/test1
+        sqrt(-1)
+        ```
+        then:
+        \input{output}{scripts/test1}
+        done.
+        """ * J.EOS |> seval
+    # errors silently
+    @test occursin("then: <pre><code></code></pre>", h)
+end
diff --git a/test/converter/hyperref.jl b/test/converter/hyperref.jl
index 9bc09bf3..3aa985a7 100644
--- a/test/converter/hyperref.jl
+++ b/test/converter/hyperref.jl
@@ -66,31 +66,3 @@ end
 
     m == "<p>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  <a id=\"$h1\"></a>\$\$\\begin{array}{c}  1+1 &=&2 \\end{array}\$\$ but further  <a id=\"$h2\"></a>\$\$\\begin{array}{c}  1 &=& 1 \\end{array}\$\$ and finally a  <span class=\"eqref)\">({{href EQR $h1}})</span> and maybe  <span class=\"eqref)\">({{href EQR $h2}})</span>.</p>\n"
 end
-
-
-@testset "Input" begin
-    #
-    # check_input_fname
-    #
-    script1 = joinpath(J.JD_PATHS[:scripts], "script1.jl")
-    write(script1, "1+1")
-    @test J.check_input_fname("script1.jl") == script1
-    @test_throws ArgumentError J.check_input_fname("script2.jl")
-
-    #
-    # resolve_input_hlcode
-    #
-    r = J.resolve_input_hlcode("script1.jl", "julia", use_hl=false)
-    r2 = J.resolve_input_othercode("script1.jl", "julia")
-    @test r == "<pre><code class=\"language-julia\">1+1\n</code></pre>"
-    @test r2 == "<pre><code class=\"language-julia\">1+1</code></pre>"
-
-    #
-    # resolve_input_plainoutput
-    #
-    plain1 = joinpath(J.JD_PATHS[:scripts], "output", "script1.zog")
-    write(plain1, "2")
-
-    r = J.resolve_input_plainoutput("script1.jl")
-    @test r == "<pre><code>2</code></pre>"
-end
diff --git a/test/converter/integration.jl b/test/converter/integration.jl
index bd6f3247..16a33590 100644
--- a/test/converter/integration.jl
+++ b/test/converter/integration.jl
@@ -56,7 +56,7 @@ end
         ```
         done
         """ * JuDoc.EOS
-    @test st |> conv == "<p>Some code <pre><code class=julia>struct P\n    x::Real\nend\n</code></pre> done</p>\n"
+    @test st |> conv == "<p>Some code <pre><code class=\"language-julia\">struct P\n    x::Real\nend\n</code></pre> done</p>\n"
 end
 
 @testset "∫ math-br" begin # see #73
@@ -151,7 +151,19 @@ end
     st |> conv == "<div class=\"emptydiv\"><div class=\"emptycore\"></div>\n</div>\n"
 end
 
-@testset "HTML escape" begin # see #151
+@testset "HTML escape" begin # see issue #151
     st = read(joinpath(D, "151.md"), String)
-    @test st |> conv == "<pre><code class=julia>add OhMyREPL#master\n</code></pre>\n<p>AAA</p>\n\n<pre><code class=\"julia\">\"\"\"\n    bar(x[, y])\n\nBBB\n\n# Examples\n```jldoctest\nD\n```\n\"\"\"\nfunction bar(x, y)\n    ...\nend\n</code></pre>\n\n<p>For complex functions with multiple arguments use a argument list, also if there are many keyword arguments use <code>&lt;keyword arguments&gt;</code>:</p>\n\n<pre><code class=\"julia\">\"\"\"\n    matdiag(diag, nr, nc; &ltkeyword arguments&gt)\n\nCreate Matrix with number `vdiag` on the super- or subdiagonals and `vndiag`\nin the rest.\n\n# Arguments\n- `diag::Number`: `Number` to write into created super- or subdiagonal\n\n# Examples\n```jldoctest\njulia> matdiag(true, 5, 5, sr=2, ec=3)\n```\n\"\"\"\nfunction\nmatdiag(diag::Number, nr::Integer, nc::Integer;)\n    ...\nend\n</code></pre>\n"
+    @test st |> conv == "<pre><code class=\"language-julia\">add OhMyREPL#master\n</code></pre>\n<p>AAA</p>\n\n<pre><code class=\"language-julia\">\"\"\"\n    bar(x[, y])\n\nBBB\n\n# Examples\n```jldoctest\nD\n```\n\"\"\"\nfunction bar(x, y)\n    ...\nend\n</code></pre>\n\n<p>For complex functions with multiple arguments use a argument list, also if there are many keyword arguments use <code>&lt;keyword arguments&gt;</code>:</p>\n\n<pre><code class=\"language-julia\">\"\"\"\n    matdiag(diag, nr, nc; &ltkeyword arguments&gt)\n\nCreate Matrix with number `vdiag` on the super- or subdiagonals and `vndiag`\nin the rest.\n\n# Arguments\n- `diag::Number`: `Number` to write into created super- or subdiagonal\n\n# Examples\n```jldoctest\njulia> matdiag(true, 5, 5, sr=2, ec=3)\n```\n\"\"\"\nfunction\nmatdiag(diag::Number, nr::Integer, nc::Integer;)\n    ...\nend\n</code></pre>\n"
+end
+
+
+@testset "∫ label" begin
+    st = raw"""
+        Blah blah
+        ## some title
+        and then an anchor here \label{anchor} done.
+        """ * J.EOS
+    J.def_GLOB_LXDEFS!()
+    r = st |> conv
+    @test occursin("here <a id=\"anchor\"></a> done.", r)
 end
diff --git a/test/converter/lx_input.jl b/test/converter/lx_input.jl
index e32fee43..12fa78cc 100644
--- a/test/converter/lx_input.jl
+++ b/test/converter/lx_input.jl
@@ -1,6 +1,39 @@
-J.JD_PATHS[:scripts] = joinpath(dirname(dirname(pathof(JuDoc))), "test", "_dummies", "scripts")
+@testset "Input" begin
+    #
+    # check_input_fname
+    #
+    script1 = joinpath(J.JD_PATHS[:assets], "script1.jl")
+    write(script1, "1+1")
+    fp, d, fn = J.check_input_rpath("script1.jl")
+    @test fp == script1
+    @test d == J.JD_PATHS[:assets]
+    @test fn == "script1"
+    @test_throws ArgumentError J.check_input_rpath("script2.jl")
+
+    #
+    # resolve_input_hlcode
+    #
+    r = J.resolve_input_hlcode("script1.jl", "julia", use_hl=false)
+    r2 = J.resolve_input_othercode("script1.jl", "julia")
+    @test r == "<pre><code class=\"language-julia\">1+1</code></pre>"
+    @test r2 == r
+
+    #
+    # resolve_input_plainoutput
+    #
+    mkpath(joinpath(J.JD_PATHS[:assets], "output"))
+    plain1 = joinpath(J.JD_PATHS[:assets], "output", "script1.out")
+    write(plain1, "2")
+
+    r = J.resolve_input_plainoutput("script1.jl")
+    @test r == "<pre><code>2</code></pre>"
+end
+
 
 @testset "LX input" begin
+    write(joinpath(J.JD_PATHS[:assets], "s1.jl"), "println(1+1)")
+    write(joinpath(J.JD_PATHS[:assets], "output", "s1a.png"), "blah")
+    write(joinpath(J.JD_PATHS[:assets], "output", "s1.out"), "blih")
     st = raw"""
         Some string
         \input{julia}{s1.jl}
@@ -17,7 +50,7 @@ J.JD_PATHS[:scripts] = joinpath(dirname(dirname(pathof(JuDoc))), "test", "_dummi
     m, _ = J.convert_md(st, collect(values(J.JD_GLOB_LXDEFS)))
     h = J.convert_html(m, J.JD_VAR_TYPE())
 
-    @test occursin("Some string <pre><code class=\"language-julia\">$(read(joinpath(J.JD_PATHS[:scripts], "s1.jl"), String))</code></pre>", h)
-    @test occursin("Then maybe <pre><code>$(read(joinpath(J.JD_PATHS[:scripts], "output", "s1.txt"), String))</code></pre>", h)
-    @test occursin("Finally img: <img src=\"/assets/scripts/output/s1a.png\" id=\"judoc-out-plot\"/> done.", h)
+    @test occursin("<p>Some string <pre><code class=\"language-julia\">$(read(joinpath(J.JD_PATHS[:assets], "s1.jl"), String))</code></pre>", h)
+    @test occursin("Then maybe <pre><code>$(read(joinpath(J.JD_PATHS[:assets], "output", "s1.out"), String))</code></pre>", h)
+    @test occursin("Finally img: <img src=\"/assets/output/s1a.png\" id=\"judoc-out-plot\"/> done.", h)
 end
diff --git a/test/jd_paths_vars.jl b/test/jd_paths_vars.jl
index 74d7d286..b2865746 100644
--- a/test/jd_paths_vars.jl
+++ b/test/jd_paths_vars.jl
@@ -14,7 +14,6 @@ J.def_GLOB_LXDEFS!()
     @test J.JD_PATHS[:libs]    == joinpath(td, "libs")
     @test J.JD_PATHS[:out]     == joinpath(td, "pub")
     @test J.JD_PATHS[:out_css] == joinpath(td, "css")
-    @test J.JD_PATHS[:scripts] == joinpath(td, "assets", "scripts")
 
     @test P == J.JD_PATHS
 
@@ -23,8 +22,7 @@ J.def_GLOB_LXDEFS!()
     mkdir(J.JD_PATHS[:libs])
     mkdir(J.JD_PATHS[:in_css])
     mkdir(J.JD_PATHS[:in_html])
-    mkpath(J.JD_PATHS[:scripts])
-    mkpath(joinpath(J.JD_PATHS[:scripts], "output"))
+    mkdir(J.JD_PATHS[:assets])
 end
 
 # copying _libs/katex in the J.JD_PATHS[:libs] so that it can be used in testing