diff --git a/.travis.yml b/.travis.yml index 5404431..fa4c131 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ language: julia julia: - 1 # current stable - - 1.0 # lts - nightly script: diff --git a/Project.toml b/Project.toml index 9ed744c..1183ac6 100644 --- a/Project.toml +++ b/Project.toml @@ -23,7 +23,7 @@ Mustache = "0.4.1, 0.5, 1" Plots = "0.28, 0.29, 1.0" Requires = "1.0" YAML = "0.3, 0.4" -julia = "1" +julia = "1.2" [extras] Cairo = "159f3aea-2a34-519c-b102-8c37f9878175" diff --git a/src/Weave.jl b/src/Weave.jl index 228ceda..02c653a 100644 --- a/src/Weave.jl +++ b/src/Weave.jl @@ -1,6 +1,6 @@ module Weave -using Highlights, Mustache, Requires +using Highlights, Mustache, Requires, Pkg # directories @@ -16,16 +16,19 @@ const WEAVE_OPTION_NAME_DEPRECATED = "options" # remove this when tagging v0.11 const WEAVE_OPTION_DEPRECATE_ID = "weave_option_duplicate_id" const DEFAULT_FIG_PATH = "figures" +const WEAVE_VERSION = try + 'v' * Pkg.TOML.parsefile(normpath(PKG_DIR, "Project.toml"))["version"] +catch + "" +end +weave_info() = WEAVE_VERSION, string(Date(now())) + function __init__() @require Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" include("plots.jl") @require Gadfly = "c91e804a-d5a3-530f-b6f0-dfbca275c004" include("gadfly.jl") end # utilitity functions -@static @isdefined(isnothing) || begin - isnothing(::Any) = false - isnothing(::Nothing) = true -end take2string!(io) = String(take!(io)) """ @@ -33,7 +36,7 @@ take2string!(io) = String(take!(io)) List supported output formats """ -list_out_formats(io = stdout) = for (k, v) in FORMATS; println(io, string(k, ": ", v.formatdict[:description])); end +list_out_formats(io = stdout) = for (k, v) in FORMATS; println(io, string(k, ": ", v.description)); end """ tangle(source::AbstractString; kwargs...) @@ -171,36 +174,40 @@ function weave( throw_errors = throw_errors, ) - # format document + # render document # --------------- - # overwrites options with those specified in header, that are needed for formatting document + # overwrites options with those specified in header, that are needed for rendering document # NOTE: these YAML options can be given dynamically if !isnothing(weave_options) if haskey(weave_options, "template") template = weave_options["template"] - # resolve relative to this document + # resolve relative to this document template isa AbstractString && (template = normpath(dirname(source), template)) end if haskey(weave_options, "css") css = weave_options["css"] - # resolve relative to this document + # resolve relative to this document css isa AbstractString && (css = normpath(dirname(source), css)) end highlight_theme = get(weave_options, "highlight_theme", highlight_theme) - pandoc_options = get(weave_options, "pandoc_options", pandoc_options) latex_cmd = get(weave_options, "latex_cmd", latex_cmd) keep_unicode = get(weave_options, "keep_unicode", keep_unicode) end - get!(doc.format.formatdict, :keep_unicode, keep_unicode) - rendered = format(doc, template, highlight_theme; css = css) + set_rendering_options!(doc; template = template, highlight_theme = highlight_theme, css = css, keep_unicode = keep_unicode) + rendered = render_doc(doc) outname = get_outname(out_path, doc) - open(io->write(io,rendered), outname, "w") # document generation via external programs + # ----------------------------------------- + + if !isnothing(weave_options) + pandoc_options = get(weave_options, "pandoc_options", pandoc_options) + end + doctype = doc.doctype if doctype == "pandoc2html" mdname = outname @@ -333,8 +340,7 @@ include("display_methods.jl") include("reader/reader.jl") include("run.jl") include("cache.jl") -include("formats.jl") -include("format.jl") +include("rendering/rendering.jl") include("pandoc.jl") include("converter.jl") diff --git a/src/display_methods.jl b/src/display_methods.jl index d5678c2..7b4933d 100644 --- a/src/display_methods.jl +++ b/src/display_methods.jl @@ -4,7 +4,7 @@ using Markdown, .WeaveMarkdown mutable struct Report <: AbstractDisplay cwd::AbstractString basename::AbstractString - formatdict::Dict{Symbol,Any} + format::WeaveFormat pending_code::AbstractString cur_result::AbstractString rich_output::AbstractString @@ -18,11 +18,11 @@ mutable struct Report <: AbstractDisplay throw_errors::Bool end -function Report(cwd, basename, formatdict, mimetypes, throw_errors) +function Report(cwd, basename, format, mimetypes, throw_errors) Report( cwd, basename, - formatdict, + format, "", "", "", diff --git a/src/format.jl b/src/format.jl deleted file mode 100644 index 8719f79..0000000 --- a/src/format.jl +++ /dev/null @@ -1,334 +0,0 @@ -# TODO: reorganize this file into multiple files corresponding to each output format - -using Mustache, Highlights, .WeaveMarkdown, Markdown, Dates, Pkg -using REPL.REPLCompletions: latex_symbols - -function format(doc, template = nothing, highlight_theme = nothing; css = nothing) - docformat = doc.format - - # Complete format dictionaries with defaults - get!(docformat.formatdict, :termstart, docformat.formatdict[:codestart]) - get!(docformat.formatdict, :termend, docformat.formatdict[:codeend]) - get!(docformat.formatdict, :out_width, nothing) - get!(docformat.formatdict, :out_height, nothing) - get!(docformat.formatdict, :fig_pos, nothing) - get!(docformat.formatdict, :fig_env, nothing) - docformat.formatdict[:highlight_theme] = get_highlight_theme(highlight_theme) - - restore_header!(doc) - - lines = map(copy(doc.chunks)) do chunk - format_chunk(chunk, docformat) - end - body = join(lines, '\n') - - return docformat isa JMarkdown2HTML ? render2html(body, doc, template, css, highlight_theme) : - docformat isa JMarkdown2tex ? render2tex(body, doc, template, highlight_theme) : - body -end - -function render2html(body, doc, template, css, highlight_theme) - _, weave_source = splitdir(abspath(doc.source)) - weave_version, weave_date = weave_info() - - return Mustache.render( - get_template(template, false); - body = body, - stylesheet = get_stylesheet(css), - highlight_stylesheet = get_highlight_stylesheet(MIME("text/html"), highlight_theme), - header_script = doc.header_script, - weave_source = weave_source, - weave_version = weave_version, - weave_date = weave_date, - [Pair(Symbol(k), v) for (k, v) in doc.header]..., - ) -end - -function render2tex(body, doc, template, highlight_theme) - return Mustache.render( - get_template(template, true); - body = body, - highlight = get_highlight_stylesheet(MIME("text/latex"), highlight_theme), - [Pair(Symbol(k), v) for (k, v) in doc.header]..., - ) -end - -get_highlight_theme(::Nothing) = Highlights.Themes.DefaultTheme -get_highlight_theme(highlight_theme::Type{<:Highlights.AbstractTheme}) = highlight_theme - -get_template(::Nothing, tex::Bool = false) = - Mustache.template_from_file(normpath(TEMPLATE_DIR, tex ? "md2pdf.tpl" : "md2html.tpl")) -get_template(path::AbstractString, tex) = Mustache.template_from_file(path) -get_template(tpl::Mustache.MustacheTokens, tex) = tpl - -get_stylesheet(::Nothing) = get_stylesheet(normpath(STYLESHEET_DIR, "skeleton.css")) -get_stylesheet(path::AbstractString) = read(path, String) - -get_highlight_stylesheet(mime, highlight_theme) = - get_highlight_stylesheet(mime, get_highlight_theme(highlight_theme)) -get_highlight_stylesheet(mime, highlight_theme::Type{<:Highlights.AbstractTheme}) = - sprint((io, x) -> Highlights.stylesheet(io, mime, x), highlight_theme) - -const WEAVE_VERSION = try - 'v' * Pkg.TOML.parsefile(normpath(PKG_DIR, "Project.toml"))["version"] -catch - "" -end - -weave_info() = WEAVE_VERSION, string(Date(now())) - -# TODO: is there any other format where we want to restore headers ? -const HEADER_PRESERVE_DOCTYPES = ("github", "hugo") - -function restore_header!(doc) - doc.doctype in HEADER_PRESERVE_DOCTYPES || return # don't restore - - # only strips Weave headers - delete!(doc.header, WEAVE_OPTION_NAME) - if haskey(doc.header, WEAVE_OPTION_NAME_DEPRECATED) - @warn "Weave: `options` key is deprecated. Use `weave_options` key instead." _id = WEAVE_OPTION_DEPRECATE_ID maxlog = 1 - delete!(doc.header, WEAVE_OPTION_NAME_DEPRECATED) - end - isempty(doc.header) && return - - # restore remained headers as `DocChunk` - header_text = "---\n$(YAML.write(doc.header))---" - pushfirst!(doc.chunks, DocChunk(header_text, 0, 0)) -end - -format_chunk(chunk::DocChunk, docformat) = join((format_inline(c) for c in chunk.content)) - -format_inline(inline::InlineText) = inline.content - -function format_inline(inline::InlineCode) - isempty(inline.rich_output) || return inline.rich_output - isempty(inline.figures) || return inline.figures[end] - return inline.output -end - -function format_chunk(chunk::DocChunk, docformat::JMarkdown2tex) - out = IOBuffer() - io = IOBuffer() - for inline in chunk.content - if isa(inline, InlineText) - write(io, inline.content) - elseif !isempty(inline.rich_output) - clear_buffer_and_format!(io, out, WeaveMarkdown.latex) - write(out, addlines(inline.rich_output, inline)) - elseif !isempty(inline.figures) - write(io, inline.figures[end], inline) - elseif !isempty(inline.output) - write(io, addlines(inline.output, inline)) - end - end - clear_buffer_and_format!(io, out, WeaveMarkdown.latex) - out = take2string!(out) - return docformat.formatdict[:keep_unicode] ? out : uc2tex(out) -end - -function format_chunk(chunk::DocChunk, docformat::JMarkdown2HTML) - out = IOBuffer() - io = IOBuffer() - for inline in chunk.content - if isa(inline, InlineText) - write(io, inline.content) - elseif !isempty(inline.rich_output) - clear_buffer_and_format!(io, out, WeaveMarkdown.html) - write(out, addlines(inline.rich_output, inline)) - elseif !isempty(inline.figures) - write(io, inline.figures[end]) - elseif !isempty(inline.output) - write(io, addlines(inline.output, inline)) - end - end - clear_buffer_and_format!(io, out, WeaveMarkdown.html) - return take2string!(out) -end - -function clear_buffer_and_format!(io::IOBuffer, out::IOBuffer, render_function) - text = take2string!(io) - m = Markdown.parse(text, flavor = WeaveMarkdown.weavemd) - write(out, string(render_function(m))) -end - -addlines(op, inline) = inline.ctype === :line ? string('\n', op, '\n') : op - -function format_chunk(chunk::CodeChunk, docformat) - formatdict = docformat.formatdict - - # Fill undefined options with format specific defaults - isnothing(chunk.options[:out_width]) && (chunk.options[:out_width] = formatdict[:out_width]) - isnothing(chunk.options[:fig_pos]) && (chunk.options[:fig_pos] = formatdict[:fig_pos]) - - # Only use floats if chunk has caption or sets fig_env - if !isnothing(chunk.options[:fig_cap]) && isnothing(chunk.options[:fig_env]) - (chunk.options[:fig_env] = formatdict[:fig_env]) - end - - haskey(formatdict, :indent) && (chunk.content = indent(chunk.content, formatdict[:indent])) - - chunk.content = format_code(chunk.content, docformat) - - if !chunk.options[:eval] - return if chunk.options[:echo] - string(formatdict[:codestart], '\n', chunk.content, formatdict[:codeend]) - else - "" - end - end - - if chunk.options[:term] - result = format_termchunk(chunk, docformat) - else - result = if chunk.options[:echo] - # Convert to output format and highlight (html, tex...) if needed - string(formatdict[:codestart], chunk.content, formatdict[:codeend], '\n') - else - "" - end - - if (strip(chunk.output) ≠ "" || strip(chunk.rich_output) ≠ "") && - (chunk.options[:results] ≠ "hidden") - if chunk.options[:results] ≠ "markup" && chunk.options[:results] ≠ "hold" - strip(chunk.output) ≠ "" && (result *= "$(chunk.output)\n") - strip(chunk.rich_output) ≠ "" && (result *= "$(chunk.rich_output)\n") - else - if chunk.options[:wrap] - chunk.output = - '\n' * wraplines(chunk.output, chunk.options[:line_width]) - chunk.output = format_output(chunk.output, docformat) - else - chunk.output = '\n' * rstrip(chunk.output) - chunk.output = format_output(chunk.output, docformat) - end - - if haskey(formatdict, :indent) - chunk.output = indent(chunk.output, formatdict[:indent]) - end - strip(chunk.output) ≠ "" && ( - result *= "$(formatdict[:outputstart])$(chunk.output)\n$(formatdict[:outputend])\n" - ) - strip(chunk.rich_output) ≠ "" && (result *= chunk.rich_output * '\n') - end - end - end - - # Handle figures - if chunk.options[:fig] && length(chunk.figures) > 0 - if chunk.options[:include] - result *= formatfigures(chunk, docformat) - end - end - - return result -end - -format_output(result, docformat) = result - -format_output(result, docformat::JMarkdown2HTML) = Markdown.htmlesc(result) - -function format_output(result, docformat::JMarkdown2tex) - # Highligts has some extra escaping defined, eg of $, ", ... - result_escaped = sprint( - (io, x) -> - Highlights.Format.escape(io, MIME("text/latex"), x, charescape = true), - result, - ) - docformat.formatdict[:keep_unicode] || return uc2tex(result_escaped, true) - return result_escaped -end - -format_code(code, docformat) = code - -# return "\\begin{minted}[mathescape, fontsize=\\small, xleftmargin=0.5em]{julia}\n$result\n\\end{minted}\n" -function format_code(code, docformat::JMarkdown2tex) - ret = highlight_code(MIME("text/latex"), code, docformat.formatdict[:highlight_theme]) - docformat.formatdict[:keep_unicode] || return uc2tex(ret) - return ret -end - -# Convert unicode to tex, escape listings if needed -function uc2tex(s, escape = false) - for key in keys(latex_symbols) - if escape - s = replace(s, latex_symbols[key] => "(*@\\ensuremath{$(texify(key))}@*)") - else - s = replace(s, latex_symbols[key] => "\\ensuremath{$(texify(key))}") - end - end - return s -end - -# Make julia symbols (\bf* etc.) valid latex -function texify(s) - return if occursin(r"^\\bf[A-Z]$", s) - replace(s, "\\bf" => "\\bm{\\mathrm{") * "}}" - elseif startswith(s, "\\bfrak") - replace(s, "\\bfrak" => "\\bm{\\mathfrak{") * "}}" - elseif startswith(s, "\\bf") - replace(s, "\\bf" => "\\bm{\\") * "}" - elseif startswith(s, "\\frak") - replace(s, "\\frak" => "\\mathfrak{") * "}" - else - s - end -end - -format_code(code, docformat::JMarkdown2HTML) = - highlight_code(MIME("text/html"), code, docformat.formatdict[:highlight_theme]) - -format_code(code, docformat::Pandoc2HTML) = - highlight_code(MIME("text/html"), code, docformat.formatdict[:highlight_theme]) - -function format_termchunk(chunk, docformat) - return if should_render(chunk) - fd = docformat.formatdict - string(fd[:termstart], chunk.output, '\n', fd[:termend], '\n') - else - "" - end -end - -format_termchunk(chunk, docformat::JMarkdown2HTML) = - should_render(chunk) ? highlight_term(MIME("text/html"), chunk.output, docformat.formatdict[:highlight_theme]) : "" - -format_termchunk(chunk, docformat::Pandoc2HTML) = - should_render(chunk) ? highlight_term(MIME("text/html"), chunk.output, docformat.formatdict[:highlight_theme]) : "" - -# return "\\begin{minted}[mathescape, fontsize=\\small, xleftmargin=0.5em]{julia}\n$result\n\\end{minted}\n" -format_termchunk(chunk, docformat::JMarkdown2tex) = - should_render(chunk) ? highlight_term(MIME("text/latex"), chunk.output, docformat.formatdict[:highlight_theme]) : "" - -should_render(chunk) = chunk.options[:echo] && chunk.options[:results] ≠ "hidden" - -highlight_code(mime, code, highlight_theme) = - highlight(mime, strip(code), Highlights.Lexers.JuliaLexer, highlight_theme) -highlight_term(mime, output, highlight_theme) = - highlight(mime, strip(output), Highlights.Lexers.JuliaConsoleLexer, highlight_theme) -highlight(mime, output, lexer, theme = Highlights.Themes.DefaultTheme) = - sprint((io, x) -> Highlights.highlight(io, mime, x, lexer, theme), output) - -indent(text, nindent) = join(map(x -> string(repeat(' ', nindent), x), split(text, '\n')), '\n') - -function wraplines(text, line_width = 75) - result = AbstractString[] - lines = split(text, '\n') - for line in lines - if length(line) > line_width - push!(result, wrapline(line, line_width)) - else - push!(result, line) - end - end - - return strip(join(result, '\n')) -end - -function wrapline(text, line_width = 75) - result = "" - while length(text) > line_width - result *= first(text, line_width) * '\n' - text = chop(text, head = line_width, tail = 0) - end - result *= text -end diff --git a/src/formats.jl b/src/formats.jl deleted file mode 100644 index 41a4d71..0000000 --- a/src/formats.jl +++ /dev/null @@ -1,481 +0,0 @@ -# TODO: -# - 1. do assertions for definition mandatory fields in `@define_format` macro -# - 2. implement fallback format/rendering functions in format.jl -# - 3. export this as public API - - -abstract type WeaveFormat end -const FORMATS = Dict{String,WeaveFormat}() - -macro define_format(type_name, supertype = :WeaveFormat) - return quote - @assert $(supertype) <: WeaveFormat "$($(supertype)) should be subtype of WeaveFormat" - struct $(type_name) <: $(supertype) - formatdict::Dict{Symbol,Any} - end - end -end -# TODO: do some assertion for necessary fields of `formatdict` -register_format!(format_name::AbstractString, format::WeaveFormat) = push!(FORMATS, format_name => format) - -# HTML -# ---- - -@define_format JMarkdown2HTML -register_format!("md2html", JMarkdown2HTML(Dict( - :description => "Julia markdown to html", - :codestart => "\n", - :codeend => "\n", - :outputstart => "
",
-    :outputend => "
\n", - :fig_ext => ".png", - :mimetypes => [ - "image/png", - "image/jpg", - "image/svg+xml", - "text/html", - "text/markdown", - "text/plain", - ], - :extension => "html", -))) - -@define_format Pandoc2HTML -register_format!("pandoc2html", Pandoc2HTML(Dict( - :description => "Markdown to HTML (requires Pandoc 2)", - :codestart => "\n", - :codeend => "\n", - :outputstart => "\n", - :outputend => "\n", - :fig_ext => ".png", - :extension => "md", - :mimetypes => [ - "image/png", - "image/svg+xml", - "image/jpg", - "text/html", - "text/markdown", - "text/plain", - ], -))) - - -# PDF and Tex -# ----------- - -@define_format JMarkdown2tex -let t = JMarkdown2tex(Dict( - :description => "Julia markdown to latex", - :codestart => "", - :codeend => "", - :outputstart => "\\begin{lstlisting}", - :outputend => "\\end{lstlisting}\n", - :fig_ext => ".pdf", - :extension => "tex", - :out_width => "\\linewidth", - :mimetypes => [ - "application/pdf", - "image/png", - "image/jpg", - "text/latex", - "text/markdown", - "text/plain", - ], - :keep_unicode => false, - )) - register_format!("md2pdf", t) - register_format!("md2tex", t) -end - -@define_format Tex -register_format!("tex", Tex(Dict( - :description => "Latex with custom code environments", - :codestart => "\\begin{juliacode}", - :codeend => "\\end{juliacode}", - :outputstart => "\\begin{juliaout}", - :outputend => "\\end{juliaout}", - :termstart => "\\begin{juliaterm}", - :termend => "\\end{juliaterm}", - :fig_ext => ".pdf", - :extension => "tex", - :out_width => "\\linewidth", - :fig_env => "figure", - :fig_pos => "htpb", - :mimetypes => ["application/pdf", "image/png", "text/latex", "text/plain"], - :keep_unicode => false, -))) -register_format!("texminted", Tex(Dict( - :description => "Latex using minted for highlighting", - :codestart => - "\\begin{minted}[mathescape, fontsize=\\small, xleftmargin=0.5em]{julia}", - :codeend => "\\end{minted}", - :outputstart => - "\\begin{minted}[fontsize=\\small, xleftmargin=0.5em, mathescape, frame = leftline]{text}", - :outputend => "\\end{minted}", - :termstart => - "\\begin{minted}[fontsize=\\footnotesize, xleftmargin=0.5em, mathescape]{jlcon}", - :termend => "\\end{minted}", - :fig_ext => ".pdf", - :extension => "tex", - :out_width => "\\linewidth", - :fig_env => "figure", - :fig_pos => "htpb", - :mimetypes => ["application/pdf", "image/png", "text/latex", "text/plain"], - :keep_unicode => false, -))) - - -# pandoc -# ------ - -@define_format Pandoc -let p = Pandoc(Dict( - :description => "Pandoc markdown", - :codestart => "~~~~{.julia}", - :codeend => "~~~~~~~~~~~~~\n\n", - :outputstart => "~~~~", - :outputend => "~~~~\n\n", - :fig_ext => ".png", - :out_width => nothing, - :extension => "md", - # Prefer png figures for markdown conversion, svg doesn't work with latex - :mimetypes => - ["image/png", "image/jpg", "image/svg+xml", "text/markdown", "text/plain"], - )) - register_format!("pandoc", p) - register_format!("pandoc2pdf", p) -end - - -# markdown -# -------- - -@define_format GitHubMarkdown -register_format!("github", GitHubMarkdown(Dict( - :description => "GitHub markdown", - :codestart => "````julia", - :codeend => "````\n\n", - :outputstart => "````", - :outputend => "````\n\n", - :fig_ext => ".png", - :extension => "md", - :mimetypes => - ["image/png", "image/svg+xml", "image/jpg", "text/markdown", "text/plain"], -))) - -@define_format Hugo -register_format!("hugo", Hugo(Dict( - :description => "Hugo markdown (using shortcodes)", - :codestart => "````julia", - :codeend => "````\n\n", - :outputstart => "````", - :outputend => "````\n\n", - :fig_ext => ".png", - :extension => "md", - :uglyURLs => false, # if `false`, prepend figure path by `..` -))) - -@define_format MultiMarkdown -register_format!("multimarkdown", MultiMarkdown(Dict( - :description => "MultiMarkdown", - :codestart => "````julia", - :codeend => "````\n\n", - :outputstart => "````", - :outputend => "````\n\n", - :fig_ext => ".png", - :extension => "md", -))) - - -# Rest -# ---- - -@define_format Rest -register_format!("rst", Rest(Dict( - :description => "reStructuredText and Sphinx", - :codestart => ".. code-block:: julia\n", - :codeend => "\n\n", - :outputstart => "::\n", - :outputend => "\n\n", - :indent => 4, - :fig_ext => ".png", - :extension => "rst", - :out_width => "15 cm", -))) - - -# Ansii -# ----- - -# asciidoc -b html5 -a source-highlighter=pygments ... -@define_format AsciiDoc -register_format!("asciidoc", AsciiDoc(Dict( - :description => "AsciiDoc", - :codestart => "[source,julia]\n--------------------------------------", - :codeend => "--------------------------------------\n\n", - :outputstart => "--------------------------------------", - :outputend => "--------------------------------------\n\n", - :fig_ext => ".png", - :extension => "txt", - :out_width => "600", -))) - - -# TODO: move this functions where used -# ------------------------------------ - -using Printf - - -function md_length_to_latex(def, reference) - if occursin("%", def) - _def = tryparse(Float64, replace(def, "%" => "")) - _def == nothing && return def - perc = round(_def / 100, digits = 2) - return "$perc$reference" - end - return def -end - -function formatfigures(chunk, docformat::JMarkdown2HTML) - fignames = chunk.figures - caption = chunk.options[:fig_cap] - width = chunk.options[:out_width] - height = chunk.options[:out_height] - f_pos = chunk.options[:fig_pos] - f_env = chunk.options[:fig_env] - result = "" - figstring = "" - - # Set size - attribs = "" - width == nothing || (attribs = "width=\"$width\"") - (attribs != "" && height != nothing) && (attribs *= ",") - height == nothing || (attribs *= " height=\"$height\" ") - - if caption != nothing - result *= """
\n""" - end - - for fig in fignames - figstring *= """\n""" - end - - result *= figstring - - if caption != nothing - result *= """ -
$caption
- """ - end - - if caption != nothing - result *= "
\n" - end - - return result -end - -function formatfigures(chunk, docformat::Union{Tex,JMarkdown2tex}) - fignames = chunk.figures - caption = chunk.options[:fig_cap] - width = chunk.options[:out_width] - height = chunk.options[:out_height] - f_pos = chunk.options[:fig_pos] - f_env = chunk.options[:fig_env] - result = "" - figstring = "" - - if f_env == nothing && caption != nothing - f_env = "figure" - end - - (f_pos == nothing) && (f_pos = "!h") - # Set size - attribs = "" - width == nothing || (attribs = "width=$(md_length_to_latex(width,"\\linewidth"))") - (attribs != "" && height != nothing) && (attribs *= ",") - height == nothing || (attribs *= "height=$(md_length_to_latex(height,"\\paperheight"))") - - if f_env != nothing - result *= "\\begin{$f_env}" - (f_pos != "") && (result *= "[$f_pos]") - result *= "\n" - end - - for fig in fignames - if splitext(fig)[2] == ".tex" # Tikz figures - figstring *= "\\resizebox{$width}{!}{\\input{$fig}}\n" - else - if isempty(attribs) - figstring *= "\\includegraphics{$fig}\n" - else - figstring *= "\\includegraphics[$attribs]{$fig}\n" - end - end - end - - # Figure environment - if caption != nothing - result *= string("\\center\n", "$figstring", "\\caption{$caption}\n") - else - result *= figstring - end - - if chunk.options[:label] != nothing && f_env != nothing - label = chunk.options[:label] - result *= "\\label{fig:$label}\n" - end - - if f_env != nothing - result *= "\\end{$f_env}\n" - end - - return result -end - -formatfigures(chunk, docformat::Pandoc2HTML) = formatfigures(chunk, pandoc) - -function formatfigures(chunk, docformat::Pandoc) - fignames = chunk.figures - length(fignames) > 0 || (return "") - - caption = chunk.options[:fig_cap] - label = get(chunk.options, :label, nothing) - result = "" - figstring = "" - attribs = "" - width = chunk.options[:out_width] - height = chunk.options[:out_height] - - # Build figure attibutes - attribs = String[] - width == nothing || push!(attribs, "width=$width") - height == nothing || push!(attribs, "height=$height") - label == nothing || push!(attribs, "#fig:$label") - attribs = isempty(attribs) ? "" : "{" * join(attribs, " ") * "}" - - if caption != nothing - result *= "![$caption]($(fignames[1]))$attribs\n" - for fig in fignames[2:end] - result *= "![]($fig)$attribs\n" - println("Warning, only the first figure gets a caption\n") - end - else - for fig in fignames - result *= "![]($fig)$attribs\\ \n\n" - end - end - return result -end - -function formatfigures(chunk, docformat::GitHubMarkdown) - fignames = chunk.figures - caption = chunk.options[:fig_cap] - result = "" - figstring = "" - - length(fignames) > 0 || (return "") - - if caption != nothing - result *= "![$caption]($(fignames[1]))\n" - for fig in fignames[2:end] - result *= "![]($fig)\n" - println("Warning, only the first figure gets a caption\n") - end - else - for fig in fignames - result *= "![]($fig)\n" - end - end - return result -end - -function formatfigures(chunk, docformat::Hugo) - relpath = docformat.formatdict[:uglyURLs] ? "" : ".." - function format_shortcode(index_and_fig) - index, fig = index_and_fig - if index > 1 - @warn("Only the first figure gets a caption.") - title_spec = "" - else - caption = chunk.options[:fig_cap] - title_spec = caption == nothing ? "" : "title=\"$(caption)\" " - end - "{{< figure src=\"$(joinpath(relpath, fig))\" $(title_spec) >}}" - end - mapreduce(format_shortcode, *, enumerate(chunk.figures), init = "") -end - -function formatfigures(chunk, docformat::MultiMarkdown) - fignames = chunk.figures - caption = chunk.options[:fig_cap] - result = "" - figstring = "" - - if chunk.options[:out_width] == nothing - width = "" - else - width = "width=$(chunk.options[:out_width])" - end - - length(fignames) > 0 || (return "") - - if caption != nothing - result *= "![$caption][$(fignames[1])]\n\n" - result *= "[$(fignames[1])]: $(fignames[1]) $width\n" - for fig in fignames[2:end] - result *= "![][$fig]\n\n" - result *= "[$fig]: $fig $width\n" - println("Warning, only the first figure gets a caption\n") - end - else - for fig in fignames - result *= "![][$fig]\n\n" - result *= "[$fig]: $fig $width\n" - end - end - return result -end - -function formatfigures(chunk, docformat::Rest) - fignames = chunk.figures - caption = chunk.options[:fig_cap] - width = chunk.options[:out_width] - result = "" - figstring = "" - - for fig in fignames - figstring *= @sprintf(".. image:: %s\n :width: %s\n\n", fig, width) - end - - if caption != nothing - result *= string( - ".. figure:: $(fignames[1])\n", - " :width: $width\n\n", - " $caption\n\n", - ) - else - result *= figstring - return result - end -end - -function formatfigures(chunk, docformat::AsciiDoc) - fignames = chunk.figures - caption = chunk.options[:fig_cap] - width = chunk.options[:out_width] - result = "" - figstring = "" - - for fig in fignames - figstring *= @sprintf("image::%s[width=%s]\n", fig, width) - end - - if caption != nothing - result *= string("image::$(fignames[1])", "[width=$width,", "title=\"$caption\"]") - else - result *= figstring - return result - end -end diff --git a/src/rendering/common.jl b/src/rendering/common.jl new file mode 100644 index 0000000..fd9767a --- /dev/null +++ b/src/rendering/common.jl @@ -0,0 +1,170 @@ +# TODO: fix terminologies: `format_foo` -> `render_foo` + +# fallback methods +# ---------------- + +set_rendering_options!(docformat::WeaveFormat; kwargs...) = return + +function restore_header!(doc) + (hasproperty(doc.format, :restore_header) && doc.format.restore_header) || return + + # only strips Weave headers + delete!(doc.header, WEAVE_OPTION_NAME) + if haskey(doc.header, WEAVE_OPTION_NAME_DEPRECATED) + @warn "Weave: `options` key is deprecated. Use `weave_options` key instead." _id = WEAVE_OPTION_DEPRECATE_ID maxlog = 1 + delete!(doc.header, WEAVE_OPTION_NAME_DEPRECATED) + end + isempty(doc.header) && return + + # restore remained headers as `DocChunk` + header_text = "---\n$(YAML.write(doc.header))---" + pushfirst!(doc.chunks, DocChunk(header_text, 0, 0)) +end + +format_chunk(chunk::DocChunk, docformat) = join((format_inline(c) for c in chunk.content)) + +format_inline(inline::InlineText) = inline.content + +function format_inline(inline::InlineCode) + isempty(inline.rich_output) || return inline.rich_output + isempty(inline.figures) || return inline.figures[end] + return inline.output +end + +function format_chunk(chunk::CodeChunk, docformat) + + # Fill undefined options with format specific defaults + isnothing(chunk.options[:out_width]) && (chunk.options[:out_width] = docformat.out_width) + isnothing(chunk.options[:fig_pos]) && (chunk.options[:fig_pos] = docformat.fig_pos) + + # Only use floats if chunk has caption or sets fig_env + if !isnothing(chunk.options[:fig_cap]) && isnothing(chunk.options[:fig_env]) + (chunk.options[:fig_env] = docformat.fig_env) + end + + hasproperty(docformat, :indent) && (chunk.content = indent(chunk.content, docformat.indent)) + + chunk.content = format_code(chunk.content, docformat) + + if !chunk.options[:eval] + return if chunk.options[:echo] + string(docformat.codestart, '\n', chunk.content, docformat.codeend) + else + "" + end + end + + if chunk.options[:term] + result = format_termchunk(chunk, docformat) + else + result = if chunk.options[:echo] + # Convert to output format and highlight (html, tex...) if needed + string(docformat.codestart, chunk.content, docformat.codeend, '\n') + else + "" + end + + if (strip(chunk.output) ≠ "" || strip(chunk.rich_output) ≠ "") && + (chunk.options[:results] ≠ "hidden") + if chunk.options[:results] ≠ "markup" && chunk.options[:results] ≠ "hold" + strip(chunk.output) ≠ "" && (result *= "$(chunk.output)\n") + strip(chunk.rich_output) ≠ "" && (result *= "$(chunk.rich_output)\n") + else + if chunk.options[:wrap] + chunk.output = + '\n' * wraplines(chunk.output, chunk.options[:line_width]) + chunk.output = format_output(chunk.output, docformat) + else + chunk.output = '\n' * rstrip(chunk.output) + chunk.output = format_output(chunk.output, docformat) + end + + hasproperty(docformat, :indent) && (chunk.output = indent(chunk.output, docformat.indent)) + + strip(chunk.output) ≠ "" && ( + result *= "$(docformat.outputstart)$(chunk.output)\n$(docformat.outputend)\n" + ) + strip(chunk.rich_output) ≠ "" && (result *= chunk.rich_output * '\n') + end + end + end + + # Handle figures + if chunk.options[:fig] && length(chunk.figures) > 0 + if chunk.options[:include] + result *= formatfigures(chunk, docformat) + end + end + + return result +end + +format_code(code, docformat) = code + +indent(text, nindent) = join(map(x -> string(repeat(' ', nindent), x), split(text, '\n')), '\n') + +function wraplines(text, line_width = 75) + result = AbstractString[] + lines = split(text, '\n') + for line in lines + if length(line) > line_width + push!(result, wrapline(line, line_width)) + else + push!(result, line) + end + end + + return strip(join(result, '\n')) +end + +function wrapline(text, line_width = 75) + result = "" + while length(text) > line_width + result *= first(text, line_width) * '\n' + text = chop(text, head = line_width, tail = 0) + end + result *= text +end + +format_output(result, docformat) = result + +function format_termchunk(chunk, docformat) + return if should_render(chunk) + string(docformat.termstart, chunk.output, '\n', docformat.termend, '\n') + else + "" + end +end + +should_render(chunk) = chunk.options[:echo] && chunk.options[:results] ≠ "hidden" + +render_doc(docformat, body, doc) = body + +# utilities +# --------- + +function clear_buffer_and_format!(io::IOBuffer, out::IOBuffer, render_function) + text = take2string!(io) + m = Markdown.parse(text, flavor = WeaveMarkdown.weavemd) + write(out, string(render_function(m))) +end + +addlines(op, inline) = inline.ctype === :line ? string('\n', op, '\n') : op + +get_template(path::AbstractString) = Mustache.template_from_file(path) +get_template(tpl::Mustache.MustacheTokens) = tpl + +get_highlight_stylesheet(mime, highlight_theme) = + get_highlight_stylesheet(mime, get_highlight_theme(highlight_theme)) +get_highlight_stylesheet(mime, highlight_theme::Type{<:Highlights.AbstractTheme}) = + sprint((io, x) -> Highlights.stylesheet(io, mime, x), highlight_theme) + +get_highlight_theme(::Nothing) = Highlights.Themes.DefaultTheme +get_highlight_theme(highlight_theme::Type{<:Highlights.AbstractTheme}) = highlight_theme + +highlight_code(mime, code, highlight_theme) = + highlight(mime, strip(code), Highlights.Lexers.JuliaLexer, highlight_theme) +highlight_term(mime, output, highlight_theme) = + highlight(mime, strip(output), Highlights.Lexers.JuliaConsoleLexer, highlight_theme) +highlight(mime, output, lexer, theme = Highlights.Themes.DefaultTheme) = + sprint((io, x) -> Highlights.highlight(io, mime, x, lexer, theme), output) diff --git a/src/rendering/htmlformats.jl b/src/rendering/htmlformats.jl new file mode 100644 index 0000000..e5f9949 --- /dev/null +++ b/src/rendering/htmlformats.jl @@ -0,0 +1,150 @@ +# HTML +# ---- + +abstract type HTMLFormat <: WeaveFormat end + +format_code(code, docformat::HTMLFormat) = + highlight_code(MIME("text/html"), code, docformat.highlight_theme) + +format_termchunk(chunk, docformat::HTMLFormat) = + should_render(chunk) ? highlight_term(MIME("text/html"), chunk.output, docformat.highlight_theme) : "" + +# Julia markdown +# -------------- + +Base.@kwdef mutable struct JMarkdown2HTML <: HTMLFormat + description = "Julia markdown to html" + extension = "html" + codestart = "\n" + codeend = "\n" + termstart = codestart + termend = codeend + outputstart = "
"
+    outputend = "
\n" + mimetypes = ["image/png", "image/jpg", "image/svg+xml", + "text/html", "text/markdown", "text/plain"] + fig_ext = ".png" + out_width = nothing + out_height = nothing + fig_pos = nothing + fig_env = nothing + # specials + template = nothing + stylesheet = nothing + highlight_theme = nothing +end +register_format!("md2html", JMarkdown2HTML()) + +function set_rendering_options!(docformat::JMarkdown2HTML; template = nothing, css = nothing, highlight_theme = nothing, kwargs...) + docformat.template = get_html_template(template) + docformat.stylesheet = get_stylesheet(css) + docformat.highlight_theme = get_highlight_theme(highlight_theme) +end + +get_html_template(::Nothing) = get_template(normpath(TEMPLATE_DIR, "md2html.tpl")) +get_html_template(x) = get_template(x) + +get_stylesheet(::Nothing) = get_stylesheet(normpath(STYLESHEET_DIR, "skeleton.css")) +get_stylesheet(path::AbstractString) = read(path, String) + +# very similar to tex version of function +function format_chunk(chunk::DocChunk, docformat::JMarkdown2HTML) + out = IOBuffer() + io = IOBuffer() + for inline in chunk.content + if isa(inline, InlineText) + write(io, inline.content) + elseif !isempty(inline.rich_output) + clear_buffer_and_format!(io, out, WeaveMarkdown.html) + write(out, addlines(inline.rich_output, inline)) + elseif !isempty(inline.figures) + write(io, inline.figures[end]) + elseif !isempty(inline.output) + write(io, addlines(inline.output, inline)) + end + end + clear_buffer_and_format!(io, out, WeaveMarkdown.html) + return take2string!(out) +end + +format_output(result, docformat::JMarkdown2HTML) = Markdown.htmlesc(result) + +function formatfigures(chunk, docformat::JMarkdown2HTML) + fignames = chunk.figures + caption = chunk.options[:fig_cap] + width = chunk.options[:out_width] + height = chunk.options[:out_height] + f_pos = chunk.options[:fig_pos] + f_env = chunk.options[:fig_env] + result = "" + figstring = "" + + # Set size + attribs = "" + isnothing(width) || (attribs = "width=\"$width\"") + (!isempty(attribs) && !isnothing(height)) && (attribs *= ",") + isnothing(height) || (attribs *= " height=\"$height\" ") + + if !isnothing(caption) + result *= """
\n""" + end + + for fig in fignames + figstring *= """\n""" + end + + result *= figstring + + if !isnothing(caption) + result *= """ +
$caption
+ """ + end + + if !isnothing(caption) + result *= "
\n" + end + + return result +end + +function render_doc(docformat::JMarkdown2HTML, body, doc; css = nothing) + _, weave_source = splitdir(abspath(doc.source)) + weave_version, weave_date = weave_info() + + return Mustache.render( + docformat.template; + body = body, + stylesheet = docformat.stylesheet, + highlight_stylesheet = get_highlight_stylesheet(MIME("text/html"), docformat.highlight_theme), + header_script = doc.header_script, + weave_source = weave_source, + weave_version = weave_version, + weave_date = weave_date, + [Pair(Symbol(k), v) for (k, v) in doc.header]..., + ) +end + +# Pandoc +# ------ + +Base.@kwdef mutable struct Pandoc2HTML <: HTMLFormat + description = "Markdown to HTML (requires Pandoc 2)" + extension = "md" + codestart = "\n" + codeend = "\n" + termstart = codestart + termend = codeend + outputstart = "\n" + outputend = "\n" + mimetypes = ["image/png", "image/svg+xml", "image/jpg", + "text/html", "text/markdown", "text/plain"] + fig_ext = ".png" + out_width = nothing + out_height = nothing + fig_pos = nothing + fig_env = nothing +end +register_format!("pandoc2html", Pandoc2HTML()) + +formatfigures(chunk, docformat::Pandoc2HTML) = formatfigures(chunk, Pandoc()) diff --git a/src/rendering/markdownformats.jl b/src/rendering/markdownformats.jl new file mode 100644 index 0000000..1858b3b --- /dev/null +++ b/src/rendering/markdownformats.jl @@ -0,0 +1,196 @@ +abstract type MarkdownFormat <: WeaveFormat end + +# GitHub markdown +# --------------- + +Base.@kwdef mutable struct GitHubMarkdown <: MarkdownFormat + description = "GitHub markdown" + extension = "md" + codestart = "````julia" + codeend = "````\n\n" + termstart = codestart + termend = codeend + outputstart = "````" + outputend = "````\n\n" + fig_ext = ".png" + mimetypes = ["image/png", "image/svg+xml", "image/jpg", + "text/markdown", "text/plain"] + out_width = nothing + out_height = nothing + fig_pos = nothing + fig_env = nothing + # specials + preserve_header = true +end +register_format!("github", GitHubMarkdown()) + +function formatfigures(chunk, docformat::GitHubMarkdown) + fignames = chunk.figures + caption = chunk.options[:fig_cap] + result = "" + figstring = "" + + length(fignames) > 0 || (return "") + + if !isnothing(caption) + result *= "![$caption]($(fignames[1]))\n" + for fig in fignames[2:end] + result *= "![]($fig)\n" + println("Warning, only the first figure gets a caption\n") + end + else + for fig in fignames + result *= "![]($fig)\n" + end + end + return result +end + +# Hugo markdown +# ------------- + +Base.@kwdef mutable struct Hugo <: MarkdownFormat + description = "Hugo markdown (using shortcodes)" + extension = "md" + codestart = "````julia" + codeend = "````\n\n" + termstart = codestart + termend = codeend + outputstart = "````" + outputend = "````\n\n" + mimetypes = default_mime_types + fig_ext = ".png" + out_width = nothing + out_height = nothing + fig_pos = nothing + fig_env = nothing + # specials + preserve_header = true + uglyURLs = false # if `false`, prepend figure path by `..` +end +register_format!("hugo", Hugo()) + +function formatfigures(chunk, docformat::Hugo) + relpath = docformat.uglyURLs ? "" : ".." + mapreduce(*, enumerate(chunk.figures), init = "") do (index, fig) + if index > 1 + @warn("Only the first figure gets a caption.") + title_spec = "" + else + caption = chunk.options[:fig_cap] + title_spec = isnothing(caption) ? "" : "title=\"$(caption)\" " + end + "{{< figure src=\"$(joinpath(relpath, fig))\" $(title_spec) >}}" + end +end + +# multi language markdown +# ----------------------- + +Base.@kwdef mutable struct MultiMarkdown <: MarkdownFormat + description = "MultiMarkdown" + extension = "md" + codestart = "````julia" + codeend = "````\n\n" + termstart = codestart + termend = codeend + outputstart = "````" + outputend = "````\n\n" + mimetypes = default_mime_types + fig_ext = ".png" + out_width = nothing + out_height = nothing + fig_pos = nothing + fig_env = nothing + # specials + preserve_header = true +end +register_format!("multimarkdown", MultiMarkdown()) + +function formatfigures(chunk, docformat::MultiMarkdown) + fignames = chunk.figures + caption = chunk.options[:fig_cap] + result = "" + figstring = "" + + if chunk.options[:out_width] == nothing + width = "" + else + width = "width=$(chunk.options[:out_width])" + end + + length(fignames) > 0 || (return "") + + if !isnothing(caption) + result *= "![$caption][$(fignames[1])]\n\n" + result *= "[$(fignames[1])]: $(fignames[1]) $width\n" + for fig in fignames[2:end] + result *= "![][$fig]\n\n" + result *= "[$fig]: $fig $width\n" + println("Warning, only the first figure gets a caption\n") + end + else + for fig in fignames + result *= "![][$fig]\n\n" + result *= "[$fig]: $fig $width\n" + end + end + return result +end + +# pandoc +# ------ + +Base.@kwdef mutable struct Pandoc <: MarkdownFormat + description = "Pandoc markdown" + extension = "md" + codestart = "~~~~{.julia}" + codeend = "~~~~~~~~~~~~~\n\n" + termstart = codestart + termend = codeend + outputstart = "~~~~" + outputend = "~~~~\n\n" + # Prefer png figures for markdown conversion, svg doesn't work with latex + mimetypes = ["image/png", "image/jpg", "image/svg+xml", + "text/markdown", "text/plain"] + fig_ext = ".png" + out_width = nothing + out_height = nothing + fig_pos = nothing + fig_env = nothing +end +register_format!("pandoc", Pandoc()) +register_format!("pandoc2pdf", Pandoc()) + +function formatfigures(chunk, docformat::Pandoc) + fignames = chunk.figures + length(fignames) > 0 || (return "") + + caption = chunk.options[:fig_cap] + label = get(chunk.options, :label, nothing) + result = "" + figstring = "" + attribs = "" + width = chunk.options[:out_width] + height = chunk.options[:out_height] + + # Build figure attibutes + attribs = String[] + isnothing(width) || push!(attribs, "width=$width") + isnothing(height) || push!(attribs, "height=$height") + isnothing(label) || push!(attribs, "#fig:$label") + attribs = isempty(attribs) ? "" : "{" * join(attribs, " ") * "}" + + if !isnothing(caption) + result *= "![$caption]($(fignames[1]))$attribs\n" + for fig in fignames[2:end] + result *= "![]($fig)$attribs\n" + println("Warning, only the first figure gets a caption\n") + end + else + for fig in fignames + result *= "![]($fig)$attribs\\ \n\n" + end + end + return result +end diff --git a/src/rendering/rendering.jl b/src/rendering/rendering.jl new file mode 100644 index 0000000..cab31a7 --- /dev/null +++ b/src/rendering/rendering.jl @@ -0,0 +1,35 @@ +# TODO: +# - 1. Improve argument handling +# - 2. Update code to use UnPack.jl to make it more readable +# - 3. Export new interface +# - 4. Document Interface + +using Mustache, Highlights, .WeaveMarkdown, Markdown, Dates, Printf +using REPL.REPLCompletions: latex_symbols + + +const FORMATS = Dict{String,WeaveFormat}() + +# TODO: do some assertion for necessary fields of `format` +register_format!(format_name::AbstractString, format::WeaveFormat) = push!(FORMATS, format_name => format) +register_format!(_, format) = error("Format needs to be a subtype of WeaveFormat.") + +set_rendering_options!(doc; kwargs...) = set_rendering_options!(doc.format; kwargs...) + +function render_doc(doc::WeaveDoc) + restore_header!(doc) + + docformat = doc.format + lines = map(copy(doc.chunks)) do chunk + format_chunk(chunk, docformat) + end + body = join(lines, '\n') + return render_doc(docformat, body, doc) +end + + +include("common.jl") +include("htmlformats.jl") +include("texformats.jl") +include("variousformats.jl") +include("markdownformats.jl") diff --git a/src/rendering/texformats.jl b/src/rendering/texformats.jl new file mode 100644 index 0000000..c8ffe5a --- /dev/null +++ b/src/rendering/texformats.jl @@ -0,0 +1,234 @@ +# Tex +# --- + +abstract type TexFormat <: WeaveFormat end + +set_rendering_options!(docformat::TexFormat; keep_unicode = false, kwargs...) = docformat.keep_unicode |= keep_unicode + +function formatfigures(chunk, docformat::TexFormat) + fignames = chunk.figures + caption = chunk.options[:fig_cap] + width = chunk.options[:out_width] + height = chunk.options[:out_height] + f_pos = chunk.options[:fig_pos] + f_env = chunk.options[:fig_env] + result = "" + figstring = "" + + if isnothing(f_env) && !isnothing(caption) + f_env = "figure" + end + + (isnothing(f_pos)) && (f_pos = "!h") + # Set size + attribs = "" + isnothing(width) || (attribs = "width=$(md_length_to_latex(width,"\\linewidth"))") + (!isempty(attribs) && !isnothing(height)) && (attribs *= ",") + isnothing(height) || (attribs *= "height=$(md_length_to_latex(height,"\\paperheight"))") + + if !isnothing(f_env) + result *= "\\begin{$f_env}" + (!isempty(f_pos)) && (result *= "[$f_pos]") + result *= "\n" + end + + for fig in fignames + if splitext(fig)[2] == ".tex" # Tikz figures + figstring *= "\\resizebox{$width}{!}{\\input{$fig}}\n" + else + if isempty(attribs) + figstring *= "\\includegraphics{$fig}\n" + else + figstring *= "\\includegraphics[$attribs]{$fig}\n" + end + end + end + + # Figure environment + if !isnothing(caption) + result *= string("\\center\n", "$figstring", "\\caption{$caption}\n") + else + result *= figstring + end + + if !isnothing(chunk.options[:label]) && !isnothing(f_env) + label = chunk.options[:label] + result *= "\\label{fig:$label}\n" + end + + if !isnothing(f_env) + result *= "\\end{$f_env}\n" + end + + return result +end + +function md_length_to_latex(def, reference) + if occursin("%", def) + _def = tryparse(Float64, replace(def, "%" => "")) + isnothing(_def) && return def + perc = round(_def / 100, digits = 2) + return "$perc$reference" + end + return def +end + +# plain Tex +# --------- + +Base.@kwdef mutable struct Tex <: TexFormat + description = "Latex with custom code environments" + extension = "tex" + codestart = "\\begin{juliacode}" + codeend = "\\end{juliacode}" + termstart = "\\begin{juliaterm}" + termend = "\\end{juliaterm}" + outputstart = "\\begin{juliaout}" + outputend = "\\end{juliaout}" + mimetypes = ["application/pdf", "image/png", "text/latex", "text/plain"] + fig_ext = ".pdf" + out_width = "\\linewidth" + out_height = nothing + fig_pos = "htpb" + fig_env = "figure" + # specials + keep_unicode = false +end +register_format!("tex", Tex()) + +# minted Tex +# ---------- + +Base.@kwdef mutable struct TexMinted <: TexFormat + description = "Latex using minted for highlighting" + extension = "tex" + codestart = "\\begin{minted}[mathescape, fontsize=\\small, xleftmargin=0.5em]{julia}" + codeend = "\\end{minted}" + termstart = "\\begin{minted}[fontsize=\\footnotesize, xleftmargin=0.5em, mathescape]{jlcon}" + termend = "\\end{minted}" + outputstart = "\\begin{minted}[fontsize=\\small, xleftmargin=0.5em, mathescape, frame = leftline]{text}" + outputend = "\\end{minted}" + mimetypes = ["application/pdf", "image/png", "text/latex", "text/plain"] + fig_ext = ".pdf" + out_width = "\\linewidth" + out_height = nothing + fig_pos = "htpb" + fig_env = "figure" + # specials + keep_unicode = false +end +register_format!("texminted", TexMinted()) + +# Tex (directly to PDF) +# --------------------- + +Base.@kwdef mutable struct JMarkdown2tex <: TexFormat + description = "Julia markdown to latex" + extension = "tex" + codestart = "" + codeend = "" + termstart = codestart + termend = codeend + outputstart = "\\begin{lstlisting}" + outputend = "\\end{lstlisting}\n" + mimetypes = ["application/pdf", "image/png", "image/jpg", + "text/latex", "text/markdown", "text/plain"] + fig_ext = ".pdf" + out_width = "\\linewidth" + out_height = nothing + fig_pos = nothing + fig_env = nothing + # specials + highlight_theme = nothing + template = nothing + keep_unicode = false +end +register_format!("md2tex", JMarkdown2tex()) +register_format!("md2pdf", JMarkdown2tex()) + +function set_rendering_options!(docformat::JMarkdown2tex; template = nothing, highlight_theme = nothing, keep_unicode = false, kwargs...) + docformat.template = get_tex_template(template) + docformat.highlight_theme = get_highlight_theme(highlight_theme) + docformat.keep_unicode |= keep_unicode +end + +get_tex_template(::Nothing) = get_template(normpath(TEMPLATE_DIR, "md2pdf.tpl")) +get_tex_template(x) = get_template(x) + +function render_doc(docformat::JMarkdown2tex, body, doc) + return Mustache.render( + docformat.template; + body = body, + highlight = get_highlight_stylesheet(MIME("text/latex"), docformat.highlight_theme), + [Pair(Symbol(k), v) for (k, v) in doc.header]..., + ) +end + +# very similar to export to html +function format_chunk(chunk::DocChunk, docformat::JMarkdown2tex) + out = IOBuffer() + io = IOBuffer() + for inline in chunk.content + if isa(inline, InlineText) + write(io, inline.content) + elseif !isempty(inline.rich_output) + clear_buffer_and_format!(io, out, WeaveMarkdown.latex) + write(out, addlines(inline.rich_output, inline)) + elseif !isempty(inline.figures) + write(io, inline.figures[end], inline) + elseif !isempty(inline.output) + write(io, addlines(inline.output, inline)) + end + end + clear_buffer_and_format!(io, out, WeaveMarkdown.latex) + out = take2string!(out) + return docformat.keep_unicode ? out : uc2tex(out) +end + +function format_output(result, docformat::JMarkdown2tex) + # Highligts has some extra escaping defined, eg of $, ", ... + result_escaped = sprint( + (io, x) -> + Highlights.Format.escape(io, MIME("text/latex"), x, charescape = true), + result, + ) + docformat.keep_unicode || return uc2tex(result_escaped, true) + return result_escaped +end + +function format_code(code, docformat::JMarkdown2tex) + ret = highlight_code(MIME("text/latex"), code, docformat.highlight_theme) + docformat.keep_unicode || return uc2tex(ret) + return ret +end + +# Convert unicode to tex, escape listings if needed +function uc2tex(s, escape = false) + for key in keys(latex_symbols) + if escape + s = replace(s, latex_symbols[key] => "(*@\\ensuremath{$(texify(key))}@*)") + else + s = replace(s, latex_symbols[key] => "\\ensuremath{$(texify(key))}") + end + end + return s +end + +# should_render(chunk) ? highlight_term(MIME("text/latex"), , docformat.highlight_theme) : "" +format_termchunk(chunk, docformat::JMarkdown2tex) = + should_render(chunk) ? highlight_term(MIME("text/latex"), chunk.output, docformat.highlight_theme) : "" + +# Make julia symbols (\bf* etc.) valid latex +function texify(s) + return if occursin(r"^\\bf[A-Z]$", s) + replace(s, "\\bf" => "\\bm{\\mathrm{") * "}}" + elseif startswith(s, "\\bfrak") + replace(s, "\\bfrak" => "\\bm{\\mathfrak{") * "}}" + elseif startswith(s, "\\bf") + replace(s, "\\bf" => "\\bm{\\") * "}" + elseif startswith(s, "\\frak") + replace(s, "\\frak" => "\\mathfrak{") * "}" + else + s + end +end diff --git a/src/rendering/variousformats.jl b/src/rendering/variousformats.jl new file mode 100644 index 0000000..9755839 --- /dev/null +++ b/src/rendering/variousformats.jl @@ -0,0 +1,86 @@ +# Rest +# ---- + +Base.@kwdef mutable struct Rest <: WeaveFormat + description = "reStructuredText and Sphinx" + extension = "rst" + codestart = ".. code-block:: julia\n" + codeend = "\n\n" + termstart = codestart + termend = codeend + outputstart = "::\n" + outputend = "\n\n" + mimetypes = default_mime_types + fig_ext = ".png" + out_width = "15 cm" + out_height = nothing + fig_pos = nothing + fig_env = nothing + # specials + indent = 4 +end +register_format!("rst", Rest()) + +function formatfigures(chunk, docformat::Rest) + fignames = chunk.figures + caption = chunk.options[:fig_cap] + width = chunk.options[:out_width] + result = "" + figstring = "" + + for fig in fignames + figstring *= @sprintf(".. image:: %s\n :width: %s\n\n", fig, width) + end + + if !isnothing(caption) + result *= string( + ".. figure:: $(fignames[1])\n", + " :width: $width\n\n", + " $caption\n\n", + ) + else + result *= figstring + return result + end +end + +# Ansii +# ----- + +# asciidoc -b html5 -a source-highlighter=pygments ... +Base.@kwdef mutable struct AsciiDoc <: WeaveFormat + description = "AsciiDoc" + extension = "txt" + codestart = "[source,julia]\n--------------------------------------" + codeend = "--------------------------------------\n\n" + termstart = codestart + termend = codeend + outputstart = "--------------------------------------" + outputend = "--------------------------------------\n\n" + mimetypes = default_mime_types + fig_ext = ".png" + out_width = "600" + out_height = nothing + fig_pos = nothing + fig_env = nothing +end +register_format!("asciidoc", AsciiDoc()) + +function formatfigures(chunk, docformat::AsciiDoc) + fignames = chunk.figures + caption = chunk.options[:fig_cap] + width = chunk.options[:out_width] + result = "" + figstring = "" + + for fig in fignames + figstring *= @sprintf("image::%s[width=%s]\n", fig, width) + end + + if !isnothing(caption) + result *= string("image::$(fignames[1])", "[width=$width,", "title=\"$caption\"]") + else + result *= figstring + return result + end +end diff --git a/src/run.jl b/src/run.jl index 69a50d0..27ff3fe 100644 --- a/src/run.jl +++ b/src/run.jl @@ -41,9 +41,9 @@ function run_doc( isnothing(mod) && (mod = sandbox = Core.eval(Main, :(module $(gensym(:WeaveSandBox)) end))::Module) @eval mod WEAVE_ARGS = $args - mimetypes = get(doc.format.formatdict, :mimetypes, default_mime_types) + mimetypes = doc.format.mimetypes - report = Report(doc.cwd, doc.basename, doc.format.formatdict, mimetypes, throw_errors) + report = Report(doc.cwd, doc.basename, doc.format, mimetypes, throw_errors) pushdisplay(report) try if cache !== :off && cache !== :refresh @@ -260,8 +260,8 @@ function eval_chunk(chunk::CodeChunk, report::Report, SandBox::Module) report.fignum = 1 report.cur_chunk = chunk - if haskey(report.formatdict, :out_width) && isnothing(chunk.options[:out_width]) - chunk.options[:out_width] = report.formatdict[:out_width] + if hasproperty(report.format, :out_width) && isnothing(chunk.options[:out_width]) + chunk.options[:out_width] = report.format.out_width end chunk.result = run_code(chunk, report, SandBox) @@ -342,13 +342,13 @@ end """Get output file name based on out_path""" function get_outname(out_path::Symbol, doc::WeaveDoc; ext = nothing) - isnothing(ext) && (ext = doc.format.formatdict[:extension]) + isnothing(ext) && (ext = doc.format.extension) outname = "$(doc.cwd)/$(doc.basename).$ext" end """Get output file name based on out_path""" function get_outname(out_path::AbstractString, doc::WeaveDoc; ext = nothing) - isnothing(ext) && (ext = doc.format.formatdict[:extension]) + isnothing(ext) && (ext = doc.format.extension) splitted = splitext(out_path) if (splitted[2]) == "" outname = "$(doc.cwd)/$(doc.basename).$ext" @@ -358,9 +358,8 @@ function get_outname(out_path::AbstractString, doc::WeaveDoc; ext = nothing) end function set_rc_params(doc::WeaveDoc, fig_path, fig_ext) - formatdict = doc.format.formatdict if isnothing(fig_ext) - doc.chunk_defaults[:fig_ext] = formatdict[:fig_ext] + doc.chunk_defaults[:fig_ext] = doc.format.fig_ext else doc.chunk_defaults[:fig_ext] = fig_ext end @@ -393,7 +392,7 @@ function collect_results(chunk::CodeChunk, fmt::ScriptResult) push!(result_chunks, rchunk) end end - if content != "" + if !isempty(content) startswith(content, "\n") || (content = "\n" * content) rchunk = CodeChunk( content, @@ -430,7 +429,7 @@ function collect_results(chunk::CodeChunk, fmt::TermResult) push!(result_chunks, rchunk) end end - if output != "" + if !isempty(output) rchunk = CodeChunk( "", chunk.number, diff --git a/src/types.jl b/src/types.jl index ddf1255..2775cef 100644 --- a/src/types.jl +++ b/src/types.jl @@ -2,6 +2,7 @@ abstract type WeaveChunk end abstract type Inline end +abstract type WeaveFormat end mutable struct WeaveDoc source::AbstractString diff --git a/templates/md2pdf.tpl b/templates/md2pdf.tpl index 08f0a1f..036358a 100644 --- a/templates/md2pdf.tpl +++ b/templates/md2pdf.tpl @@ -7,6 +7,7 @@ \usepackage{graphicx} \usepackage{microtype} \usepackage{hyperref} + \setlength{\parindent}{0pt} \setlength{\parskip}{1.2ex} diff --git a/test/formatter_test.jl b/test/formatter_test.jl index 5093752..1a2e1b3 100644 --- a/test/formatter_test.jl +++ b/test/formatter_test.jl @@ -26,7 +26,7 @@ parsed = Weave.WeaveDoc("documents/chunk_options.noweb") doc = run_doc(parsed, doctype = "md2html") c_check = "
\nx = [12, 10]\nprintln(y)\n
\n" -doc.format.formatdict[:highlight_theme] = DefaultTheme +doc.format.highlight_theme = DefaultTheme c = Weave.format_code(doc.chunks[3].content, doc.format) @test c_check == c @@ -39,7 +39,7 @@ parsed = Weave.WeaveDoc("documents/chunk_options.noweb") doc = run_doc(parsed, doctype = "md2tex") c_check = "\\begin{lstlisting}\n(*@\\HLJLnf{println}@*)(*@\\HLJLp{(}@*)(*@\\HLJLn{x}@*)(*@\\HLJLp{)}@*)\n\\end{lstlisting}\n" -doc.format.formatdict[:highlight_theme] = DefaultTheme +doc.format.highlight_theme = DefaultTheme c = Weave.format_code(doc.chunks[4].content, doc.format) @test c_check == c @@ -93,7 +93,7 @@ fmt = deepcopy(Weave.FORMATS["md2tex"]) f = Weave.format_chunk(chunk, fmt) @test f == "\\section{Test chunk}\n\\ensuremath{\\alpha}\n\n" -fmt.formatdict[:keep_unicode] = true +fmt.keep_unicode = true f = Weave.format_chunk(chunk, fmt) @test f == "\\section{Test chunk}\nα\n\n" @@ -104,13 +104,14 @@ str = """ ``` """ doc = mock_doc(str; doctype = "md2tex") -doc = Weave.format(doc) +Weave.set_rendering_options!(doc.format) +doc = Weave.render_doc(doc) @test occursin(Weave.uc2tex("α"), doc) @test !occursin("α", doc) doc = mock_doc(str; doctype = "md2tex") -doc.format.formatdict[:keep_unicode] = true -doc = Weave.format(doc) +Weave.set_rendering_options!(doc.format; keep_unicode = true) +doc = Weave.render_doc(doc) @test occursin("α", doc) @test !occursin(Weave.uc2tex("α"), doc) diff --git a/test/test_chunk_options.jl b/test/test_chunk_options.jl index 8d44828..91ba5f9 100644 --- a/test/test_chunk_options.jl +++ b/test/test_chunk_options.jl @@ -1,8 +1,6 @@ using Weave: parse_options, parse_markdown -@static @isdefined(hasproperty) || (hasproperty(x, s::Symbol) = s in propertynames(x)) - @testset "`parse_options`" begin # general