Skip to content

Commit

Permalink
Add sc and sco (#128)
Browse files Browse the repository at this point in the history
  • Loading branch information
rikhuijzer authored Jun 21, 2021
1 parent d353c62 commit ba352b5
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 161 deletions.
53 changes: 53 additions & 0 deletions docs/contents/demo.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,59 @@ This way, you can pass a informative path with plots for which informative capti
To define the labels and/or captions manually, see @sec:labels-captions.
For showing multiple plots, see @sec:plots.

Most things can be done via functions.
However, defining a struct is not possible, because `@sco` cannot locate the struct definition inside the module.
Therefore, it is also possible to pass code and specify that you want to evaluate and show code (sc) without showing the output:

<pre>
```jl
sc("
struct Point
x
y
end
")
```
</pre>

```jl
sc("
struct Point
x
y
end
")
```

and show code and output (sco).
For example,

<pre>
```jl
sco("p = Point(1, 2)")
```
</pre>

shows as

```jl
sco("p = Point(1, 2)")
```

Note that this is starting to look a lot like R Markdown where the syntax would be something like

<pre>
```{r, results='hide'}
x = rnorm(100)
```
</pre>

I guess that there is no perfect way here.
The benefit of evaluating the user input directly, as Books.jl is doing, seems to be that it is more extensible if I'm not mistaken.
Possibly, the reasoning is that R Markdown needs to convert the output directly, whereas Julia's better type system allows for converting in much later stages, but I'm not sure.

> **Tip**: After you run `gen(; M)` with the `Point` struct defined above, the struct will be available in your REPL.
## Labels and captions {#sec:labels-captions}

To set labels and captions, wrap your object in `Options`:
Expand Down
2 changes: 1 addition & 1 deletion src/Books.jl
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ include("generate.jl")
export html, pdf, docx, build_all
export code, ImageOptions, Options
export code_block
export @sc, CodeAndFunction, @sco
export @sc, sc, CodeAndFunction, @sco, sco
export gen
export serve

Expand Down
31 changes: 15 additions & 16 deletions src/generate.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ extract_expr_example() = """
```jl
foo(3)
```
```jl
foo(3)
bar
```
ipsum `jl bar()` dolar
"""

Expand All @@ -27,8 +31,9 @@ Here, `s` is the contents of a Markdown file.
julia> s = Books.extract_expr_example();
julia> Books.extract_expr(s)
2-element Vector{String}:
3-element Vector{String}:
"foo(3)"
"foo(3)\\nbar"
"bar()"
```
"""
Expand All @@ -49,7 +54,7 @@ function extract_expr(s::AbstractString)::Vector

function check_parse_errors(expr)
try
Meta.parse(expr)
Meta.parse("begin $expr end")
catch e
error("Exception occured when trying to parse `$expr`")
end
Expand Down Expand Up @@ -100,7 +105,6 @@ julia> Books.method_name("Options(foo(); caption='b')")
function method_name(expr::String)
remove_macros(expr) = replace(expr, r"@[\w\_]*" => "")
expr = remove_macros(expr)
# These rewrites are not reversible, because they do not have to be.
expr = replace(expr, '(' => '_')
expr = replace(expr, ')' => "")
expr = replace(expr, ';' => "_")
Expand All @@ -119,23 +123,17 @@ Escape an expression to the corresponding path.
The logic in this method should match the logic in the Lua filter.
"""
function escape_expr(expr::String)
replace_map = [
'(' => "-ob-",
')' => "-cb-",
'"' => "-dq-",
':' => "-fc-",
';' => "-sc-",
'@' => "-ax-"
]
escaped = reduce(replace, replace_map; init=expr)
escaped = 60 < length(expr) ? expr[1:60] : expr
escaped = replace(escaped, r"([^a-zA-Z0-9]+)" => "_")
joinpath(GENERATED_DIR, "$escaped.md")
end

function evaluate_and_write(M::Module, expr::String)
path = escape_expr(expr)
println("Writing output of `$expr` to $path")
expr_info = replace(expr, '\n' => "\\n")
println("Writing output of `$expr_info` to $path")

ex = Meta.parse(expr)
ex = Meta.parse("begin $expr end")
out = Core.eval(M, ex)
out = convert_output(expr, path, out)
out = string(out)::String
Expand All @@ -148,7 +146,8 @@ function evaluate_and_write(f::Function)
function_name = Base.nameof(f)
expr = "$(function_name)()"
path = escape_expr(expr)
println("Writing output of `$expr` to $path")
expr_info = replace(expr, '\n' => "\\n")
println("Writing output of `$expr_info` to $path")
out = f()
out = convert_output(expr, path, out)
out = string(out)::String
Expand Down Expand Up @@ -242,7 +241,7 @@ julia> module Foo
julia> call_html = false; # To avoid Pandoc errors breaking this jldoctest.
julia> gen(Foo.version; call_html)
Writing output of `version()` to _gen/version-ob--cb-.md
Writing output of `version()` to _gen/version_.md
```
"""
function gen(f::Function; project="default", call_html=true)
Expand Down
91 changes: 40 additions & 51 deletions src/include-codeblocks.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,18 @@ local function shift_headings(blocks, shift_by)
end

--- Return path of the markdown file for the string `s` given by the user.
---
--- We can just drop a lot of info because it will probably still be unique.
--- Otherwise, Julia can detect duplicate filenames and throw an error.
---
--- Ensure that this logic corresponds to the logic inside Books.jl.
---
local md_path
function md_path(s)
-- Escape all weird characters to ensure they can be in the file.
-- This yields very weird names, but luckily the code is only internal.
escaped = s
escaped = escaped:gsub("%(", "-ob-")
escaped = escaped:gsub("%)", "-cb-")
escaped = escaped:gsub("\"", "-dq-")
escaped = escaped:gsub(":", "-fc-")
escaped = escaped:gsub(";", "-sc-")
escaped = escaped:gsub("@", "-ax-")
path_sep = package.config:sub(1,1)
escaped = string.sub(s, 1, 60)
escaped = escaped:gsub("([^a-zA-Z0-9]+)", "_")
-- Platform independent path separator.
path_sep = package.config:sub(1, 1)
path = "_gen" .. path_sep .. escaped .. ".md"
return path
end
Expand Down Expand Up @@ -86,48 +85,38 @@ function transclude_codeblock(cb)
local buffer_last_heading_level = last_heading_level

local blocks = List:new()
for line in cb.text:gmatch('[^\n]+') do
if line:sub(1,2) ~= '//' then

path = md_path(line)
if 100 < path:len() then
msg = "ERROR: The text `" .. line .. "` is too long to be converted to a filename"
msg = { pandoc.CodeBlock(msg) }
blocks:extend(msg)
-- Lua has no continue.
goto skip_to_next
end

local fh = io.open(path)
if not fh then
not_found_error(line, path, '```')
suggestion = "Did you run `gen(; M)` where `M = YourModule`?\n"
msg = "ERROR: Cannot find file at " .. path .. " for `" .. line .. "`."
msg = msg .. ' ' .. suggestion
msg = { pandoc.CodeBlock(msg) }
blocks:extend(msg)
else
local text = fh:read("*a")
local contents = pandoc.read(text, format).blocks
last_heading_level = 0
-- recursive transclusion
contents = pandoc.walk_block(
-- Here, the contents is added as an Any block.
-- Then, the filter is applied again recursively because
-- the included file could contain an include again!
pandoc.Div(contents),
{ Header = update_last_level, CodeBlock = transclude }
).content
--- reset to level before recursion
last_heading_level = buffer_last_heading_level
contents = shift_headings(contents, shift_heading_level_by)
-- Note that contents has type List.
blocks:extend(contents)
fh:close()
end
end
::skip_to_next::


path = md_path(cb.text)

local fh = io.open(path)
if not fh then
not_found_error(cb.text, path, '```')
suggestion = "Did you run `gen(; M)` where `M = YourModule`?\n"
msg = "ERROR: Cannot find file at " .. path .. " for `" .. cb.text .. "`."
msg = msg .. ' ' .. suggestion
msg = { pandoc.CodeBlock(msg) }
blocks:extend(msg)
else
local text = fh:read("*a")
local contents = pandoc.read(text, format).blocks
last_heading_level = 0
-- recursive transclusion
contents = pandoc.walk_block(
-- Here, the contents is added as an Any block.
-- Then, the filter is applied again recursively because
-- the included file could contain an include again!
pandoc.Div(contents),
{ Header = update_last_level, CodeBlock = transclude }
).content
--- reset to level before recursion
last_heading_level = buffer_last_heading_level
contents = shift_headings(contents, shift_heading_level_by)
-- Note that contents has type List.
blocks:extend(contents)
fh:close()
end
::skip_to_next::
return blocks
end

Expand Down
92 changes: 0 additions & 92 deletions src/include-files.lua

This file was deleted.

Loading

0 comments on commit ba352b5

Please sign in to comment.