-
Notifications
You must be signed in to change notification settings - Fork 0
/
gif.jl
134 lines (116 loc) · 5.65 KB
/
gif.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
"""
save_code_gif(output_path, code_string; delay=0.25, font_size=28, height=nothing, allow_errors=false)
Given Julia source code as a string, run the code in a REPL mode and save the results as a gif to `output_path`.
"""
function save_code_gif(output_path, code_string; delay=0.25, font_size=28, height=nothing, allow_errors=false, mod=get_module())
cast = _cast_str(code_string, delay; height, allow_errors, mod)
save_gif(output_path, cast::Cast; font_size)
return output_path
end
"""
save_gif(output_path, cast::Cast; font_size=28)
Saves the [`Cast`](@ref) to `output_path` as a gif (using [`agg`](https://github.com/asciinema/agg)).
"""
function save_gif(output_path, cast::Cast; font_size=28)
mktempdir() do tmp
input_path = joinpath(tmp, "input.cast")
save(input_path, cast)
# Larger font size to reduce blurriness:
# https://github.com/asciinema/agg/issues/60#issuecomment-1807910643
run(pipeline(`$(agg()) --font-size $(font_size) $input_path $output_path`; stdout=devnull, stderr=devnull))
end
return output_path
end
function get_attribute(attributes, key, default)
idx = findfirst(attr -> attr[1] == key, attributes)
if idx === nothing
value = default
else
value = tryparse(typeof(default), attributes[idx][2])
if value === nothing
@warn "Invalid $(key) $(attributes[idx][2]). Using default value $default."
value = default
end
end
return value
end
# Pandoc filter to add gifs with the contents of `julia {cast="true"}` code blocks.
function cast_action(tag, content, format, meta; base_dir, counter, module_meta)
tag == "CodeBlock" || return nothing
length(content) < 2 && return nothing
length(content[1]) < 3 && return nothing
isempty(content[1][2]) && return nothing
content[1][2][1] == "julia" || return nothing
attributes = content[1][3]
["cast", "true"] in attributes || return nothing
font_size = get_attribute(attributes, "font-size", 28)
delay = get_attribute(attributes, "delay", 0.25)
height = get_attribute(attributes, "height", 0)
name_idx = findfirst(attr -> attr[1] == "name", attributes)
example_name = isnothing(name_idx) ? nothing : attributes[name_idx][2]
allow_errors = get_attribute(attributes, "allow_errors", false)
if height == 0
height = nothing
end
block = content[2]
counter[] += 1
c = counter[]
rel_path = joinpath("assets", "output_$(c)_@cast.gif")
mod = Documenter.get_sandbox_module!(module_meta, "@cast", example_name)
save_code_gif(joinpath(base_dir, rel_path), block; delay, font_size, height, allow_errors, mod)
return [
Pandoc.CodeBlock(content...)
Pandoc.Para([Pandoc.Image(["", [], []], [], [rel_path, ""])])
]
end
# Pandoc filter to remove gifs that contain `@cast` in their path
function rm_old_gif(tag, content, format, meta)
tag == "Image" || return nothing
path = content[3][1]
contains(path, "@cast") || return nothing
endswith(path, ".gif") || return nothing
return []
end
"""
cast_readme(MyPackage::Module)
cast_readme(MyPackage::Module, output_path)
Add gifs for each `julia {cast="true"}` code-block in the README of MyPackage. This is just a smaller helper that calls [`cast_document`](@ref) on `joinpath(pkgdir(MyPackage), "README.md")`.
See [`cast_document`](@ref) for more options and warnings.
"""
cast_readme(mod::Module) = cast_document(joinpath(pkgdir(mod), "README.md"))
cast_readme(mod::Module, output_path) = cast_document(joinpath(pkgdir(mod), "README.md"), output_path)
"""
cast_document(input_path, output_path=input_path; format="gfm+attributes")
For each `julia {cast="true"}` code-block in the input document, generates a gif
executing that code in a REPL, saves it to `joinpath(dirname(output_path), "assets")`
and inserts it as an image following the code-block, writing the resulting document
to `output_path`.
The default `format` is Github-flavored markdown. Specify the `format` keyword argument to choose an alternate pandoc-supported format (see <https://pandoc.org/MANUAL.html#general-options>). This has only been tested with Github-flavored markdown, but theoretically should work with any pandoc format.
Returns the number of gifs generated.
!!! warning
This function relies on parsing the document using pandoc and roundtripping through the pandoc AST. This can result in unexpected modifications to the document.
It is recommended to check your document into source control before running this function,
or specifying an `output_path` that is different from the input path, in order to assess
the results.
"""
function cast_document(input_path, output_path=input_path; format="gfm+attributes", hacky_fix=true)
json = JSON3.read(read(`$(pandoc()) --wrap=preserve -f $format -t json $input_path`), Dict)
base_dir = dirname(output_path)
mkpath(joinpath(base_dir, "assets"))
counter = Ref{Int}(0)
module_meta = Dict{Symbol, Any}()
act = (args...) -> cast_action(args...; base_dir, counter, module_meta)
output = JSON3.write(Pandoc.filter(json, [rm_old_gif, act]))
open(`$(pandoc()) -f json -t $format --wrap=preserve --resource-path=$(base_dir) -o $output_path`; write=true) do io
write(io, output)
end
if hacky_fix
# We do this since GitHub doesn't seem to display syntax highlighting
# on READMEs for "``` {.julia}", although VSCode does.
# I also think ```julia is less ugly than ``` {.julia}.
str = read(output_path, String)
str = replace(str, r"^``` {.julia "m => "```julia {", r"^``` julia"m => "```julia")
write(output_path, str)
end
return counter[]
end