Skip to content

Commit

Permalink
REPL mode for code blocks (#1035)
Browse files Browse the repository at this point in the history
* repl mode #818
  • Loading branch information
tlienart authored Jul 11, 2023
1 parent 5629fc5 commit 6cae15e
Show file tree
Hide file tree
Showing 11 changed files with 442 additions and 27 deletions.
109 changes: 109 additions & 0 deletions docs/code/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,115 @@ savefig(joinpath(@OUTPUT, "sinc.svg")) # hide

**Note**: If you wish to use `Plots.jl` and deploy to GitHub pages, you will need to modify the `.github/workflows/Deploy.yml` by adding `env: GKSwstype: "100"` before the ` - name: Build and Deploy` line. [Here](https://github.com/storopoli/Bayesian-Julia/blob/master/.github/workflows/Deploy.yml) is an example.

### Auto and REPL mode

You can use `!` and `>` to indicate respectively a code that should be run automatically
and the output appended immediately after, or the same but with a REPL-style display:

````plaintext
```!
x = 5
y = x^2
```
````

for instance gives:

```!
x = 5
y = x^2
```

In a similar way:

````plaintext
```>
x = 5
y = x^2
```
````

gives

```>
x = 5
y = x^2
```

**Shell, Pkg, Help**, these modes are also experimentally supported:

_Pkg mode_ :

````
```]
st
```
````

gives

```]
st
```

_Shell mode_ : (**note**: in a multi-line setting, each line is assumed to be a separate command)

````
```;
echo "hello!"
date
```
````

gives

```;
echo "hello!"
date
```

~~~
<style>
.julia-help {
background-color: #fffee0;
padding: 10px;
font-style: italic;
}
.julia-help h1,h2,h3 {
font-size: 1em;
font-weight: 500;
}
</style>
~~~

_Help mode_ : (**note**: only single line cell blocks will work properly)

````
```?
im
```
````

```?
im
```

**Note**: for the `help` mode above, the output is HTML corresponding to the julia
docs, it's wrapped in a `julia-help` div which you should style, the above style
for instance corresponds to the following CSS:

```css
.julia-help {
background-color: #fffee0;
padding: 10px;
font-style: italic;
}
.julia-help h1,h2,h3 {
font-size: 1em;
font-weight: 500;
}
```

### Troubleshooting

A few things can go wrong when attempting to use and evaluate code blocks.
Expand Down
6 changes: 5 additions & 1 deletion src/converter/markdown/blocks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ function convert_block(β::AbstractBlock, lxdefs::Vector{LxDef})::AS

βn == :CODE_INLINE && return html_code_inline(stent(β) |> htmlesc)
βn == :CODE_BLOCK_LANG && return resolve_code_block.ss)
βn == :CODE_BLOCK! && return resolve_code_block.ss, shortcut=true)
βn == :CODE_BLOCK! && return resolve_code_block.ss; shortcut=true)
βn == :CODE_REPL && return resolve_code_block.ss; repl=true)
βn == :CODE_PKG && return resolve_code_block.ss; pkg=true)
βn == :CODE_HELP && return resolve_code_block.ss; help=true)
βn == :CODE_SHELL && return resolve_code_block.ss; shell=true)
βn == :CODE_BLOCK && return html_code(stent(β), "{{fill lang}}")
βn == :CODE_BLOCK_IND && return convert_indented_code_block.ss)

Expand Down
127 changes: 111 additions & 16 deletions src/eval/codeblock.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ $SIGNATURES
Take a fenced code block and return a tuple with the language, the relative
path (if any) and the code.
"""
function parse_fenced_block(ss::SubString, shortcut=false)::Tuple
if shortcut
function parse_fenced_block(ss::SubString; shortcut=false,
repl=false, shell=false, pkg=false, help=false)::Tuple

if any((shortcut, repl, shell, pkg, help))
lang = locvar(:lang)::String
cntr = locvar(:fd_evalc)::Int
rpath = "_ceval_$cntr"
Expand Down Expand Up @@ -86,9 +88,16 @@ Helper function to process the content of a code block.
Return the html corresponding to the code block, possibly after having
evaluated the code.
"""
function resolve_code_block(ss::SubString; shortcut=false)::String
function resolve_code_block(
ss::SubString;
shortcut=false,
repl=false,
pkg=false,
shell=false,
help=false
)::String
# 1. what kind of code is it
lang, rpath, code = parse_fenced_block(ss, shortcut)
lang, rpath, code = parse_fenced_block(ss; shortcut, repl, pkg, shell, help)
# 1.a if no rpath is given, code should not be evaluated
isnothing(rpath) && return html_code(code, lang)
# 1.b if not julia code, eval is not supported
Expand All @@ -105,10 +114,11 @@ function resolve_code_block(ss::SubString; shortcut=false)::String
# languages, can't do the module trick so will need to keep track
# of that virtually. There will need to be a branching over lang=="julia"
# vs rest here.
repl_code_chunks = Pair{String,String}[]

# 2. here we have Julia code, assess whether to run it or not
# if not, just return the code as a html block
if should_eval(code, rpath)
if any((shortcut, repl, shell, help, pkg)) || should_eval(code, rpath)
# 3. here we have code that should be (re)evaluated
# >> retrieve the modulename, the module may not exist
# (& may not need to)
Expand All @@ -117,6 +127,7 @@ function resolve_code_block(ss::SubString; shortcut=false)::String
mod = ismodule(modname) ?
getfield(Main, Symbol(modname)) :
newmodule(modname)

# >> retrieve the code paths
cp = form_codepaths(rpath)
# >> write the code to file
Expand All @@ -129,22 +140,106 @@ function resolve_code_block(ss::SubString; shortcut=false)::String
out = ifelse(locvar(:auto_code_path)::Bool, cp.out_dir, bk)
isdir(out) || mkpath(out)
cd(out)
# >> eval the code in the relevant module (this creates output/)
res = run_code(mod, code, cp.out_path; strip_code=false)
cd(bk)
# >> write res to file
# >> this weird thing with QuoteNode is to make sure that the proper
# "show" method is called...
io = IOBuffer()
Core.eval(mod, quote show($(io), "text/plain", $(QuoteNode(res))) end)
write(cp.res_path, take!(io))

if repl
# imitating https://github.com/JuliaLang/julia/blob/fe2eeadc0b382508bef7e77ab517789ea844e708/stdlib/REPL/src/REPL.jl#L429-L430
chunk_code = ""
chunk_ast = nothing
for line in split(code, r"\r?\n", keepempty=false)
chunk_code *= line * "\n"
chunk_ast = Base.parse_input_line(chunk_code)
if (isa(chunk_ast, Expr) && chunk_ast.head === :incomplete)
continue
else
# we have a complete chunk of code
# >> eval the code in the relevant module (this creates output/)
res = run_code(mod, chunk_code, cp.out_path; strip_code=false)
cd(bk)
# >> write res to string (see further down)
io = IOBuffer()
Core.eval(mod, quote show($(io), "text/plain", $(QuoteNode(res))) end)
stdout_str = read(cp.out_path, String)
res_str = String(take!(io))
res_str = ifelse(res_str == "nothing", "", res_str * "\n")
push!(repl_code_chunks,
chunk_code => stdout_str * res_str
)
# reset for the next chunk
chunk_code = ""
chunk_ast = nothing
end
end

# NOTE: shell, pkg, and help mode are currently fairly rudimentary
# and should be considered experimental

elseif shell
for line in split(code, '\n', keepempty=false)
a = tempname()
open(a, "w") do outf
redirect_stdout(outf) do
redirect_stderr(outf) do
Base.repl_cmd(Cmd(string.(split(line))), nothing)
end
end
end
push!(repl_code_chunks,
line => String(strip(read(a, String))) * "\n"
)
end

elseif pkg
for line in split(code, '\n', keepempty=false)
a = tempname()
pname = splitpath(Pkg.project().path)[end-1]
open(a, "w") do outf
redirect_stdout(outf) do
redirect_stderr(outf) do
Pkg.REPLMode.pkgstr(string(line))
end
end
end
push!(repl_code_chunks,
"($(pname)) pkg> " * line => String(strip(read(a, String))) * "\n"
)
end

elseif help
# NOTE: this is pretty crap there should be a better way to just
# reproduce what `?` but the code for the Docs module is opaque to me.
r = eval(Meta.parse("@doc $code"))
push!(repl_code_chunks,
code => replace(Markdown.html(r),
"<a href=\"@ref\">" => "",
"</code></a>" => "</code>",
"language-jldoctest" => "language-julia-repl"
)
)

else
# >> eval the code in the relevant module (this creates output/)
res = run_code(mod, code, cp.out_path; strip_code=false)
cd(bk)
# >> write res to file
# >> this weird thing with QuoteNode is to make sure that the proper
# "show" method is called...
io = IOBuffer()
Core.eval(mod, quote show($(io), "text/plain", $(QuoteNode(res))) end)
write(cp.res_path, take!(io))
end
# >> since we've evaluated a code block, toggle scope as stale
set_var!(LOCAL_VARS, "fd_eval", true)
end
# >> finally return as html
if locvar(:showall)::Bool || shortcut
# >> finally return as html either with or without output
# --- with
if any((repl, shell, help, pkg))
s = repl ? :repl : shell ? :shell : help ? :help : :pkg
return html_repl_code(repl_code_chunks, s)

elseif shortcut || locvar(:showall)::Bool
return html_code(code, lang) *
reprocess("\\show{$rpath}", [GLOBAL_LXDEFS["\\show"]])
end
# --- without
return html_code(code, lang)
end
5 changes: 5 additions & 0 deletions src/manager/franklin.jl
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,11 @@ function serve(; clear::Bool = false,

# check if a Project.toml file is available, if so activate the folder
flag_env = false

if isfile(joinpath(FOLDER_PATH[], "Project.toml"))
Pkg.activate(".")
flag_env = true
set_var!(GLOBAL_VARS, "_has_base_project", true)
end

# construct the set of files to watch
Expand Down Expand Up @@ -210,11 +212,14 @@ function fd_fullpass(watched_files::NamedTuple, join_to_prepath::String="")::Int
# reset global page variables and latex definitions
# NOTE: need to keep track of pre-path if specified, see optimize
prepath = get(GLOBAL_VARS, "prepath", "")
has_base_project = globvar(:_has_base_project)

def_GLOBAL_VARS!()
def_GLOBAL_LXDEFS!()
empty!.((RSS_ITEMS, SITEMAP_DICT))
# reinsert prepath if specified
isempty(prepath) || (GLOBAL_VARS["prepath"] = prepath)
set_var!(GLOBAL_VARS, "_has_base_project", has_base_project)

# process configuration file (see also `process_mddefs!`)
# note the order (utils the config) is important, see also #774
Expand Down
13 changes: 13 additions & 0 deletions src/manager/write_page.jl
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,19 @@ function convert_and_write(root::String, file::String, head::String,
# conversion
content = convert_md(read(fpath, String))

# check if the active project has changed, if so change it back
blackhole = IOBuffer()
if globvar(:_has_base_project)::Bool
if Pkg.project().path != joinpath(FOLDER_PATH[], "Project.toml")
Pkg.activate(".", io=blackhole)
end
else
if isnothing(match(r"v\d\.\d+", splitpath(Pkg.project().path)[end-1]))
Pkg.activate(io=blackhole)
end
end


# 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 ¯\_(ツ)_/¯
Expand Down
Loading

0 comments on commit 6cae15e

Please sign in to comment.