"""
save(f, tree)
Wrapper function for `FileTrees.save` to configure whether to use parallel
saving using `FileTrees` or to just use a basic serial implementation.
Typically the simpler serial code will be faster unless the project is very
large.
"""
function save(f, tree)
if DAGGER[]
FileTrees.save(f, tree)
else
for file in FileTrees.files(tree)
dir = dirname(file)
isdir(dir) || mkpath(dir)
f(file)
end
end
end
HTML
"""
Convert the given `src` project to a collection of HTML files.
"""
function html(src, dst=nothing; keywords...)
p = Project(src; keywords...)
p === nothing && return src
h = p.env["publish"]["html"]
h["template"]["string"] = String(exec(p.tree[h["template"]["file"]][]))
sandbox(dst) do
default_html_pages(p) # TODO: handle as part of template?
tree = rename(p.tree, pwd())
mapping = page_neighbours(p.pages)
searchmd = p"search.md"
tree = touch(tree, searchmd; value=load_markdown("# Search\n<div id='search-results'></div>"))
mapping[searchmd] = (prev=searchmd, next=searchmd)
save(f -> _html(p, tree, f, mapping), tree)
end
return src
end
function init(p::Project, ::typeof(html); port=nothing, dir=nothing, kws...)
html(p, dir; kws...)
LiveServer.serve(; port=port, dir=dir)
end
_html(p::Project, t::FileTree, f::File, m::Dict) = _html(p, exec(f[]), relative(Path(path(f)), basename(t)), m)
function _html(p::Project, node::CommonMark.Node, path::AbstractPath, mapping::Dict)
if haskey(mapping, path)
dst = with_extension(path, "html")
# Setup.
let pub = p.env["publish"]
pub["mapping"] = mapping
pub["smartlink-engine"] = (_,_,n,_)->toc_link(n, p, p.env["publish"], path)
ast = exec(p.tree[pub["toc"]][])
pub["html"]["toc"] = CommonMark.html(ast, p.env["publish"])
pub["html"]["prev"], pub["html"]["next"] = mapping[path]
end
# Writing.
relative_paths(p, path) do pub
pub["html"]["prev"] = with_extension(pub["html"]["prev"], "html")
pub["html"]["next"] = with_extension(pub["html"]["next"], "html")
dir, name = splitdir(dst)
cd(isempty(dir) ? "." : dir) do
open(name, "w") do io
pub["template-engine"] = Mustache.render
pub["smartlink-engine"] = (_,_,n,_)->html_link(n, p, pub, path)
CommonMark.html(io, node, pub)
end
end
end
write("search.json", JSON.json(json_search_data(p)))
# Cleanup.
let pub = p.env["publish"]
delete!(pub, "mapping")
delete!(pub, "template-engine")
delete!(pub, "smartlink-engine")
delete!(pub["html"], "toc")
delete!(pub["html"], "prev")
delete!(pub["html"], "next")
end
end
return nothing
end
_html(::Project, data::Vector{UInt8}, path::AbstractPath, ::Dict) = write(path, data)
_html(p::Project, t::FileTrees.Thunk, path::AbstractPath, env::Dict) = _html(p, exec(t), path, env)
_html(::Project, ::Any, ::AbstractPath, ::Dict) = nothing
function toc_link(node, project, pub, path)
obj = deepcopy(node.t)
# Change to toc location based on the current path.
reltoc = relpath(pub["toc"], dirname(joinpath(".", string(path))))
# Adjust the toc link's path based on the new toc root.
obj.destination = joinpath(dirname(reltoc), obj.destination)
return html_link(obj, node, project, pub, path)
end
html_link(node, project, pub, path) = html_link(deepcopy(node.t), node, project, pub, path)
function html_link(obj, node, project, pub, path)
if obj.destination == "#"
function docs_func!(literal::AbstractString)
dict = frontmatter(exec(project.tree[path][]))
module_binding = binding(get(dict, "module", findmodule(project.env)))
if Docs.defined(module_binding)
target_binding = binding(Docs.resolve(module_binding), literal)
if Docs.defined(target_binding)
rel = relpath("docstrings/$target_binding.html", string(dirname(path)))
obj.destination = rel
@goto END
end
end
@warn "cross-reference link '$literal' on page '$path' cound not be found."
@label END
return nothing
end
function header_func!(literal::AbstractString)
slug = CommonMark.slugify(literal)
for each in project.pages, (node, enter) in exec(project.tree[each][])
if enter && get(node.meta, "id", nothing) == slug
name = with_extension(relpath(each, dirname(path)), "html")
obj.destination = "$name#$slug"
obj.title = ""
@goto END
end
end
@warn "cross-reference link '$literal' on page '$path' could not be found."
@label END
return nothing
end
# `#` is used for cross-references. The link is determined by either
# the provided `.title` field of the link, or the contents of the link.
if isempty(obj.title)
# No title provided so we use the contents of the link.
(!CommonMark.isnull(node.first_child) && node.first_child.t isa CommonMark.Code) ?
docs_func!(node.first_child.literal) : header_func!(node.first_child.literal)
else
# The `.title` is available, so use that to determine the link.
m = match(r"^`(.+)`$", obj.title)
m === nothing ? header_func!(obj.title) : docs_func!(m[1])
end
elseif startswith(obj.destination, "#")
# Skip these kind of links, they're just page-local.
else
dst = Path(normpath(dirname(string(path)), obj.destination))
if haskey(pub["mapping"], dst)
# If it's in the project's page mapping then we change the extension.
obj.destination = with_extension(obj.destination, "html")
end
end
return obj
end
function default_html_pages(p::Project)
if !isempty(p.pages)
content =
"""
<!DOCTYPE html>
<html>
<head>
<meta http-equiv = "refresh" content = "0; url = $(with_extension(first(p.pages), "html"))" />
</head>
</html>
"""
write("index.html", content)
end
return nothing
end
"""
Extract JSON search data from a project for use in lunr.js.
"""
function json_search_data(project::Project)
dict = Dict{String,String}()
root = dirname(joinpath(".", project.env["publish"]["toc"]))
for page in project.pages
path = relpath(string(page), root)
path = with_extension(path, "html")
id = path
if hasfile(project.tree, page)
for (node, enter) in exec(project.tree[page][])
if enter
if haskey(node.meta, "id")
id = "$path#$(node.meta["id"])"
end
if (node.t isa CommonMark.Text || node.t isa CommonMark.Code)
if haskey(dict, id)
dict[id] = "$(dict[id]) $(node.literal)"
else
dict[id] = node.literal
end
end
end
end
end
end
json = Dict{String,String}[]
for (id, body) in dict
push!(json, Dict("id" => id, "body" => body))
end
return json
end
HTML - print layout. Self-contained with all local assets inlined.
function html_doc(src, dst=nothing; keywords...)
p = Project(src; keywords...)
p === nothing && return src
sandbox(dst) do
Replace the default html
format with html_doc
override.
html = p.env["publish"]["html_doc"]
p.env["publish"]["html"] = html
Read in the template.
html["template"]["string"] = String(exec(p.tree[html["template"]["file"]][]))
Move the project tree into the sandboxed directory.
tree = rename(p.tree, pwd())
Capture the HTML output of each page in a dict since save
does not
give a stable ordering. We’ll write them in the correct order afterwards.
pages = Dict()
save(f -> _html_doc(pages, p, tree, f), tree)
Make a mock AST to pass to the final html
writer.
buf = IOBuffer()
for (nth, page) in enumerate(p.pages)
println(buf, "<div id='source-page-$nth'>", pages[page], "</div>")
end
ast = CommonMark.Node(CommonMark.HtmlBlock())
ast.literal = String(take!(buf))
Data-ify all requested resources.
for key in ["default_js", "default_css", "js", "css"]
if haskey(html, key)
p.env["publish"]["html"][key] = [_base64resource(file) for file in html[key]]
end
end
Setup the template renderer and write output.
p.env["publish"]["template-engine"] = Mustache.render
Write the main project file.
tocroot = joinpath(".", dirname(p.env["publish"]["toc"]))
project_file = joinpath(tocroot, p.env["name"] * ".html")
open(project_file, "w") do handle
CommonMark.html(handle, ast, p.env["publish"])
end
Clear up other files, we should leave the destination directory clear
aside from the single .html
project file.
for each in readdir(pwd())
each == basename(project_file) || rm(each; recursive=true)
end
end
return src
end
const BASE64_MIMES = Dict(
".css" => "text/css",
".js" => "text/javascript",
".png" => "image/png",
".svg" => "image/svg+xml",
".jpg" => "image/jpeg",
".jpeg" => "image/jpeg",
)
function _base64resource(file::AbstractString)
if isfile(file)
_, ext = splitext(file)
mime = get(BASE64_MIMES, ext, "text/plain")
bin = Base64.base64encode(read(file, String))
return "data:$mime;base64,$bin"
else
return file
end
end
function _html_doc(io, p::Project, tree, path, node::CommonMark.Node)
p.env["publish"]["smartlink-engine"] = (_, _, n, _) -> _html_doc_link(n)
io[path] = CommonMark.html(node, p.env["publish"])
end
_inline_image(t::CommonMark.Image) = _base64resource(t.destination)
_inline_image(t::CommonMark.Link) = t.destination
_html_doc(io, p::Project, t::FileTree, f::File) = _html_doc(io, p, t, relative(Path(path(f)), basename(t)), exec(f[]))
_html_doc(io, p::Project, tree, path, thunk::FileTrees.Thunk) = _html_doc(io, p, tree, path, exec(thunk))
_html_doc(io, p::Project, tree, path, data::Vector{UInt8}) = write(path, data)
_html_doc(io, project, tree, path, other) = nothing
function _html_doc_link(node::CommonMark.Node)
dst = node.t.destination
link = deepcopy(node.t)
if dst == "#"
TODO: handle docstrings.
literal = if isempty(node.t.title)
node.first_child.literal
else
node.t.title
end
id = CommonMark.slugify(literal)
link.destination = "#$id"
elseif startswith(dst, "#")
Ignore these, they already point to somewhere on this page.
else
TODO: handle full-page links.
end
return link
end
"""
Convert the given `src` project to a PDF file.
"""
function pdf(src, dst=nothing; keywords...)
p = Project(src; keywords...)
p === nothing && return nothing
sandbox(dst) do
tree = rename(p.tree, pwd())
save(f -> _pdf(p, tree, f), tree)
tocroot = joinpath(".", dirname(p.env["publish"]["toc"]))
io = IOBuffer()
println(io, "```{=latex}")
for page in p.pages
rpath = relpath(string(page), tocroot)
name, _ = splitext(rpath)
name = unix_style_path(name) # Path adjustments for Windows.
folder = dirname(joinpath(".", name))
println(io, "\\import{$folder/}{$(basename(name)).tex}")
end
println(io, "```")
ast = load_markdown(io)
project_file = joinpath(tocroot, p.env["name"] * ".tex")
t = p.env["publish"]["latex"]
t["template"]["string"] = String(exec(p.tree[t["template"]["file"]][]))
p.env["publish"]["template-engine"] = Mustache.render
open(project_file, "w") do handle
CommonMark.latex(handle, ast, p.env["publish"])
end
# Build the final PDF document using tectonic.
We have two options that need to be handled here. Either a
bibliography file has been provided, in which case we must run
biber
manually to correctly render bibliographies. The other is the
standard case, where no biblatex bibliography has been provided.
tectonic_args = p.env["publish"]["tectonic"]["args"]::Vector
if haskey(t, "bibliography")
Tectonic.tectonic() do bin
run(`$bin $(tectonic_args...) --keep-intermediates --reruns 0 $project_file`)
end
Tectonic.Biber.biber() do bin
run(`$bin $(first(splitext(project_file)))`)
end
end
Tectonic.tectonic() do path
run(`$path $(tectonic_args...) $project_file`)
end
end
return src
end
function init(p::Project, ::typeof(pdf); dir=nothing, kws...)
pdf(p, dir; kws...)
pdf_viewer = Sys.iswindows() ? "start" : Sys.isapple() ? "open" : "xdg-open"
run(`$pdf_viewer $(joinpath(dir, p.env["name"] * ".pdf"))`)
end
_pdf(p::Project, t::FileTree, f::File) = _pdf(p, exec(f[]), relative(Path(path(f)), basename(t)))
function _pdf(p::Project, node::CommonMark.Node, path::AbstractPath)
pub = p.env["publish"]
pub["smartlink-engine"] = (_, _, n, _) -> tex_link(n)
dst = with_extension(path, "tex")
dir, name = splitdir(dst)
cd(isempty(dir) ? "." : dir) do
open(name, "w") do io
path_hash = CommonMark.slugify(string(path))
println(io, "\\hypertarget{page-$path_hash}{}")
CommonMark.latex(io, node, pub)
end
end
end
_pdf(::Project, data::Vector{UInt8}, path::AbstractPath) = write(path, data)
_pdf(::Project, ::Any, ::AbstractPath) = nothing
function tex_link(n::CommonMark.Node)
TODO: handle docstrings.
link = n.t
if link.destination == "#"
We don’t need to do any link resolving, since this is done by the TeX engine for us. Instead we just mark ‘internal’ links with a leading ‘#’ character and external ones without.
link = deepcopy(link)
literal = CommonMark.slugify(isempty(link.title) ? n.first_child.literal : link.title)
link.destination = "#$literal"
elseif endswith(link.destination, ".md")
path_hash = CommonMark.slugify(link.destination)
link = deepcopy(link)
link.destination = "#page-$path_hash"
end
return link
end