diff --git a/src/converter/markdown/blocks.jl b/src/converter/markdown/blocks.jl index 21cb0eb31..f2aadab8e 100644 --- a/src/converter/markdown/blocks.jl +++ b/src/converter/markdown/blocks.jl @@ -15,6 +15,7 @@ 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_REPL && return resolve_code_block(β.ss, repl=true) βn == :CODE_BLOCK && return html_code(stent(β), "{{fill lang}}") βn == :CODE_BLOCK_IND && return convert_indented_code_block(β.ss) diff --git a/src/eval/codeblock.jl b/src/eval/codeblock.jl index 93a6f5915..273b11100 100644 --- a/src/eval/codeblock.jl +++ b/src/eval/codeblock.jl @@ -9,8 +9,8 @@ $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)::Tuple + if shortcut || repl lang = locvar(:lang)::String cntr = locvar(:fd_evalc)::Int rpath = "_ceval_$cntr" @@ -86,9 +86,9 @@ 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)::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) # 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 @@ -105,10 +105,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 shortcut || repl || 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) @@ -117,6 +118,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 @@ -129,22 +131,54 @@ 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) + push!(repl_code_chunks, + chunk_code => String(take!(io)) + ) + # reset for the next chunk + chunk_code = "" + chunk_ast = nothing + end + end + 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 repl + return html_repl_code(repl_code_chunks) + elseif shortcut || locvar(:showall)::Bool return html_code(code, lang) * reprocess("\\show{$rpath}", [GLOBAL_LXDEFS["\\show"]]) end + # --- without return html_code(code, lang) end diff --git a/src/parser/markdown/tokens.jl b/src/parser/markdown/tokens.jl index 00b9bf5bb..8f46ebc4b 100644 --- a/src/parser/markdown/tokens.jl +++ b/src/parser/markdown/tokens.jl @@ -88,15 +88,16 @@ const MD_TOKENS = LittleDict{Char, Vector{TokenFinder}}( '`' => [ isexactly("`", ('`',), false) => :CODE_SINGLE, # `⎵ isexactly("``", ('`',), false) => :CODE_DOUBLE, # ``⎵* # 3+ can be named - isexactly("```", SPACE_CHAR) => :CODE_TRIPLE, # ```⎵* - isexactly("```!", SPACE_CHAR) => :CODE_TRIPLE!,# ```!⎵* - is_language(3) => :CODE_LANG3, # ```lang* - isexactly("````", SPACE_CHAR) => :CODE_QUAD, # ````⎵* - is_language(4) => :CODE_LANG4, # ````lang* - isexactly("`````", SPACE_CHAR) => :CODE_PENTA, # `````⎵* - is_language(5) => :CODE_LANG5, # `````lang* + isexactly("```", SPACE_CHAR) => :CODE_TRIPLE, # ```⎵* + isexactly("```!", SPACE_CHAR) => :CODE_TRIPLE!, # ```!⎵* + isexactly("```>", SPACE_CHAR) => :CODE_REPL, # ```>⎵* + is_language(3) => :CODE_LANG3, # ```lang* + isexactly("````", SPACE_CHAR) => :CODE_QUAD, # ````⎵* + is_language(4) => :CODE_LANG4, # ````lang* + isexactly("`````", SPACE_CHAR) => :CODE_PENTA, # `````⎵* + is_language(5) => :CODE_LANG5, # `````lang* ], - '*' => [ incrlook(is_hr3) => :HORIZONTAL_RULE, + '*' => [ incrlook(is_hr3) => :HORIZONTAL_RULE, ] ) # end dict #= NOTE @@ -148,6 +149,7 @@ const MD_OCB = [ OCProto(:CODE_BLOCK_LANG, :CODE_LANG4, (:CODE_QUAD,) ), OCProto(:CODE_BLOCK_LANG, :CODE_LANG5, (:CODE_PENTA,) ), OCProto(:CODE_BLOCK!, :CODE_TRIPLE!, (:CODE_TRIPLE,) ), + OCProto(:CODE_REPL, :CODE_REPL, (:CODE_TRIPLE,) ), OCProto(:CODE_BLOCK, :CODE_TRIPLE, (:CODE_TRIPLE,) ), OCProto(:CODE_BLOCK, :CODE_QUAD, (:CODE_QUAD,) ), OCProto(:CODE_BLOCK, :CODE_PENTA, (:CODE_PENTA,) ), @@ -256,7 +258,13 @@ CODE_BLOCKS_NAMES List of names of code blocks environments. """ -const CODE_BLOCKS_NAMES = (:CODE_BLOCK_LANG, :CODE_BLOCK, :CODE_BLOCK!, :CODE_BLOCK_IND) +const CODE_BLOCKS_NAMES = ( + :CODE_BLOCK_LANG, + :CODE_BLOCK, + :CODE_BLOCK!, + :CODE_REPL, + :CODE_BLOCK_IND +) """ MD_CLOSEP diff --git a/src/regexes.jl b/src/regexes.jl index 804c1934e..98b7f4b53 100644 --- a/src/regexes.jl +++ b/src/regexes.jl @@ -69,7 +69,7 @@ const FN_DEF_PAT = r"^\[\^[\p{L}0-9_]+\](:)?$" CODE blocks ===================================================== =# -const CODE_3!_PAT = r"```\!\s*\n?((?:.|\n)*)```" +const CODE_3!_PAT = r"```(?:\!|\>)\s*\n?((?:.|\n)*)```" const CODE_3_PAT = Regex( "```([a-zA-Z][a-zA-Z-]*)" * # language diff --git a/src/utils/html.jl b/src/utils/html.jl index 609691ae3..f1caf4404 100644 --- a/src/utils/html.jl +++ b/src/utils/html.jl @@ -109,6 +109,23 @@ Convenience function to introduce inline code. """ html_code_inline(c::AS) = "$c" + +""" + html_repl_code +""" +function html_repl_code(chunks::Vector{Pair{String,String}})::String + isempty(chunks) && return "" + io = IOBuffer() + println(io, "
")
+    for (code, result) in chunks
+        println(io, "julia> " * htmlesc(strip(code)))
+        println(io, result)
+    end
+    println(io, "
") + return String(take!(io)) +end + + """ html_err diff --git a/test/eval/repl.jl b/test/eval/repl.jl new file mode 100644 index 000000000..36d87f8d2 --- /dev/null +++ b/test/eval/repl.jl @@ -0,0 +1,27 @@ +@testset "parse fenced" begin + s = """ + ```> + x = 5 + y = 7 + x + ``` + some explanation + ```> + z = y * 2 + ``` + """ |> fd2html + + @test isapproxstr(s, """ +

+        julia> x = 5
+        5
+        julia> y = 7 + x
+        12
+        
+ +

some explanation

+

+        julia> z = y * 2
+        24
+        
+ """) +end diff --git a/test/runtests.jl b/test/runtests.jl index fc9b26206..7acb336cf 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -56,6 +56,7 @@ include("eval/codeblock.jl") include("eval/eval.jl") include("eval/integration.jl") include("eval/extras.jl") +include("eval/repl.jl") # LATEX println("LATEX")