diff --git a/CHANGELOG.md b/CHANGELOG.md index e8f17fc416..7370b63ba6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -134,6 +134,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * The `doctest` routine can now receive the same `plugins` keyword argument as `makedocs`. This enables `doctest` to run if any plugin with a mandatory `Plugin` object is loaded, e.g., [DocumenterCitations](https://github.com/JuliaDocs/DocumenterCitations.jl). ([#2245]) +* The HTML output will automatically write larger `@example`-block outputs to files, to make the generated HTML files smaller. The size threshold can be controlled with the `example_size_threshold` option to `HTML`. ([#2143], [#2247]) + ### Fixed * Line endings in Markdown source files are now normalized to `LF` before parsing, to work around [a bug in the Julia Markdown parser][julia-29344] where parsing is sensitive to line endings, and can therefore cause platform-dependent behavior. ([#1906]) diff --git a/Project.toml b/Project.toml index fbfaec6ed7..a68a2815ff 100644 --- a/Project.toml +++ b/Project.toml @@ -20,6 +20,7 @@ Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" RegistryInstances = "2792f1a3-b283-48e8-9a74-f99dce5104f3" +SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Unicode = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" diff --git a/src/html/HTMLWriter.jl b/src/html/HTMLWriter.jl index 47f474175f..7ddf2789e3 100644 --- a/src/html/HTMLWriter.jl +++ b/src/html/HTMLWriter.jl @@ -44,6 +44,8 @@ using Dates: Dates, @dateformat_str, now import Markdown using MarkdownAST: MarkdownAST, Node import JSON +import Base64 +import SHA import ..Documenter using Documenter: NavNode @@ -360,6 +362,12 @@ value is `"en"`. **`warn_outdated`** inserts a warning if the current page is not the newest version of the documentation. +**`example_size_threshold`** specifies the size threshold above which the `@example` and other block +outputs get written to files, rather than being included in the HTML page. This mechanism is present +to reduce the size of the generated HTML files that contain a lot of figures etc. +Setting it to `nothing` will disable writing to files, and setting to `0` means that all examples +will be written to files. Defaults to `8 KiB`. + **`size_threshold`** sets the maximum allowed HTML file size (in bytes) that Documenter is allowed to generate for a page. If the generated HTML file is larged than this, Documenter will throw an error and the build will fail. If set to `nothing`, the file sizes are not checked. Defaults to `200 KiB` (but @@ -450,6 +458,7 @@ struct HTML <: Documenter.Writer size_threshold :: Int size_threshold_warn :: Int size_threshold_ignore :: Vector{String} + example_size_threshold :: Int function HTML(; prettyurls :: Bool = true, @@ -471,9 +480,13 @@ struct HTML <: Documenter.Writer prerender :: Bool = false, node :: Union{Cmd,String,Nothing} = nothing, highlightjs :: Union{String,Nothing} = nothing, - size_threshold :: Union{Integer, Nothing} = 200 * 2^10, - size_threshold_warn :: Union{Integer, Nothing} = 100 * 2^10, + size_threshold :: Union{Integer, Nothing} = 200 * 2^10, # 200 KiB + size_threshold_warn :: Union{Integer, Nothing} = 100 * 2^10, # 100 KiB size_threshold_ignore :: Vector = String[], + # The choice of the default here is that having ~10 figures on a page + # seems reasonable, and that would lead to ~80 KiB, which is still fine + # and leaves a buffer before hitting `size_threshold_warn`. + example_size_threshold :: Union{Integer, Nothing} = 8 * 2^10, # 8 KiB # deprecated keywords edit_branch :: Union{String, Nothing, Default} = Default(nothing), @@ -519,11 +532,16 @@ struct HTML <: Documenter.Writer elseif size_threshold_warn > size_threshold throw(ArgumentError("size_threshold_warn ($size_threshold_warn) must be smaller than size_threshold ($size_threshold)")) end + if isnothing(example_size_threshold) + example_size_threshold = typemax(Int) + elseif example_size_threshold < 0 + throw(ArgumentError("example_size_threshold must be non-negative, got $(example_size_threshold)")) + end isa(edit_link, Default) && (edit_link = edit_link[]) new(prettyurls, disable_git, edit_link, repolink, canonical, assets, analytics, collapselevel, sidebar_sitename, highlights, mathengine, description, footer, ansicolor, lang, warn_outdated, prerender, node, highlightjs, - size_threshold, size_threshold_warn, size_threshold_ignore, + size_threshold, size_threshold_warn, size_threshold_ignore, example_size_threshold, ) end end @@ -580,6 +598,12 @@ struct SearchRecord text :: String end +Base.@kwdef struct AtExampleFallbackWarning + page::String + size_bytes::Int + fallback::Union{String,Nothing} +end + """ [`HTMLWriter`](@ref)-specific globals that are passed to `domify` and other recursive functions. @@ -594,12 +618,14 @@ mutable struct HTMLContext search_index :: Vector{SearchRecord} search_index_js :: String search_navnode :: Documenter.NavNode -end + atexample_warnings::Vector{AtExampleFallbackWarning} -HTMLContext(doc, settings=nothing) = HTMLContext( - doc, settings, [], "", "", "", [], "", - Documenter.NavNode("search", "Search", nothing), -) + HTMLContext(doc, settings=nothing) = new( + doc, settings, [], "", "", "", [], "", + Documenter.NavNode("search", "Search", nothing), + AtExampleFallbackWarning[], + ) +end struct DCtx # ctx and navnode were recursively passed to all domify() methods @@ -724,6 +750,34 @@ function render(doc::Documenter.Document, settings::HTML=HTML()) @debug "Rendering $(page) [$(repr(idx))]" render_page(ctx, nn) end + # ctx::HTMLContext might have accumulated some warnings about large at-example blocks + # If so, we'll report them here in one big warning (rather than one each). + if !isempty(ctx.atexample_warnings) + msg = """ + For $(length(ctx.atexample_warnings)) @example blocks, the 'text/html' representation of the resulting + object is above the the threshold (example_size_threshold: $(ctx.settings.example_size_threshold) bytes). + """ + fallbacks = unique(w.fallback for w in ctx.atexample_warnings) + # We'll impose some regular order, but importantly we want 'nothing'-s on the top + for fallback in sort(fallbacks, by = s -> isnothing(s) ? "" : s) + warnings = filter(w -> w.fallback == fallback, ctx.atexample_warnings) + n_warnings = length(warnings) + largest_size = maximum(w -> w.size_bytes, warnings) + msg *= if isnothing(fallback) + """ + - $(n_warnings) blocks had no image MIME show() method representation as an alternative. + Sticking to the 'text/html' representation (largest block is $(largest_size) bytes). + """ + else + """ + - $(n_warnings) blocks had '$(fallback)' fallback image representation available, using that. + """ + end + pages = sort(unique(w.page for w in warnings)) + msg *= " On pages: $(join(pages, ", "))\n" + end + @warn(msg) + end # Check that all HTML files are smaller or equal to size_threshold option all(size_limit_successes) || throw(HTMLSizeThresholdError()) @@ -1770,6 +1824,93 @@ function get_url(ctx, path::AbstractString) end end +""" +Generates a unique file for the output of an at-example block if it goes over the configured +size threshold, and returns the filename (that should be in the same directory are the +corresponding HTML file). If the data is under the threshold, no file is created, and the +function returns `nothing`. +""" +function write_data_file(dctx::DCtx, data::Union{Vector{UInt8},AbstractString}; suffix::AbstractString) + ctx, navnode = dctx.ctx, dctx.navnode + # If we're under the threshold, we return `nothing`, indicating to the caller that + # they should inline the file instead. + if length(data) < ctx.settings.example_size_threshold + return nothing + end + slug = dataslug(data) + datafile = data_filename(dctx, slug, suffix) + mkpath(dirname(datafile.path)) # generally, the directory for the HTML page will not exist yet + write(datafile.path, data) + # In all cases the file should be places in the same directory as the HTML file, + # so we only need the filename to generate a valid relative href. + return datafile.filename +end + +function data_filename(dctx::DCtx, slug::AbstractString, suffix::AbstractString) + ctx, navnode = dctx.ctx, dctx.navnode + # We want to + dir, pagename = splitdir(navnode.page) + # Let's normalize the filename of the page by removing .md extensions (if present). + # We'll keep other extensions though. + if endswith(pagename, ".md") + pagename = first(splitext(pagename)) + end + + filename_prefix = if ctx.settings.prettyurls + # If pretty URLs are enabled, we would normally have + # foo/bar.md -> foo/bar/index.html, and we then generate files + # like foo/bar/$(slug).png + # However, if we have foo/index.md, then it becomes foo/index.html, + # and so we want to differentiate the data filenames just in case, + # and so they become foo/index-$(slug).png + if pagename == "index" + string("index-", slug) + else + # We also need to update dir from foo/ to foo/bar here, since we want the + # file to end up at foo/bar/$(slug).png + dir = joinpath(dir, pagename) + slug + end + else + # If pretty URLs are disabled, then + # foo/bar.md becomes foo/bar.html, and we always want to add the + # Markdown filename to the data filename, i.e. foo/bar-$(slug).png + string(pagename, "-", slug) + end + # Now we need to find a valid file name, in case there are existing duplicates. + filename = find_valid_data_file(joinpath(ctx.doc.user.build, dir), filename_prefix, suffix) + return (; + filename, + path = joinpath(ctx.doc.user.build, dir, filename), + ) +end + +function find_valid_data_file(directory::AbstractString, prefix::AbstractString, suffix::AbstractString) + # We'll try 10_000 different filename.. if this doesn't work, then something is probably really + # badly wrong, and so we just crash. + for i in 0:10_000 + filename = if i == 0 + string(prefix, suffix) + else + string(prefix, '-', lpad(string(i), 3, '0'), suffix) + end + ispath(joinpath(directory, filename)) || return filename + end + error(""" + Unable to find valid file name for an at-example output: + directory = $(directory) + prefix = $(prefix) + suffix = $(suffix)""") +end + +""" +Returns the first `limit` characters of the hex SHA1 of the data `bytes`. +""" +function dataslug(bytes::Union{Vector{UInt8},AbstractString}; limit=8)::String + full_sha = bytes2hex(SHA.sha1(bytes)) + return first(full_sha, limit) +end + """ Returns the full path of a [`Documenter.NavNode`](@ref) relative to `src/`. """ @@ -2148,12 +2289,29 @@ domify(dctx::DCtx, node::Node, moe::Documenter.MultiOutputElement) = Base.invoke function domify(dctx::DCtx, node::Node, d::Dict{MIME,Any}) rawhtml(code) = Tag(Symbol("#RAW#"))(code) - return if haskey(d, MIME"text/html"()) - rawhtml(d[MIME"text/html"()]) - elseif haskey(d, MIME"image/svg+xml"()) + # Our first preference for the MIME type is 'text/html', which we can natively include + # in the HTML. But it might happen that it's too large (above example_size_threshold), + # in which case we move on to trying to write it as an image. + has_text_html = false + if haskey(d, MIME"text/html"()) + if length(d[MIME"text/html"()]) < dctx.ctx.settings.example_size_threshold + # If the size threshold is met, we can just return right away + return rawhtml(d[MIME"text/html"()]) + end + # We'll set has_text_html to distinguish it from the case where the 'text/html' MIME + # show() method was simply missing. + has_text_html = true + end + # If text/html failed, we try to write the output as an image, possibly to a file. + image = if haskey(d, MIME"image/svg+xml"()) + @tags img svg = d[MIME"image/svg+xml"()] svg_tag_match = match(r"]*>", svg) - if svg_tag_match === nothing + dom = if length(svg) >= dctx.ctx.settings.example_size_threshold + filename = write_data_file(dctx, svg; suffix=".svg") + @assert !isnothing(filename) + img[:src => filename, :alt => "Example block output"] + elseif svg_tag_match === nothing # There is no svg tag so we don't do any more advanced # processing and just return the svg as HTML. # The svg string should be invalid but that's not our concern here. @@ -2193,16 +2351,43 @@ function domify(dctx::DCtx, node::Node, d::Dict{MIME,Any}) rawhtml(string("")) end - + (; dom = dom, mime = "image/svg+xml") elseif haskey(d, MIME"image/png"()) - rawhtml(string("")) + domify_show_image_binary(dctx, "png", d) elseif haskey(d, MIME"image/webp"()) - rawhtml(string("")) + domify_show_image_binary(dctx, "webp", d) elseif haskey(d, MIME"image/gif"()) - rawhtml(string("")) + domify_show_image_binary(dctx, "gif", d) elseif haskey(d, MIME"image/jpeg"()) - rawhtml(string("")) - elseif haskey(d, MIME"text/latex"()) + domify_show_image_binary(dctx, "jpeg", d) + end + # If image is nothing, then the object did not have any of the supported image + # representations. In that case, if the 'text/html' representation exists, we use that, + # but with a warning because it goes over the size limit. If 'text/html' was missing too, + # we carry on to additional MIME types. + if has_text_html && isnothing(image) + # The 'text/html' representation of an @example block is above the threshold, but no + # supported image representation is present as an alternative. + push!(dctx.ctx.atexample_warnings, AtExampleFallbackWarning( + page = dctx.navnode.page, + size_bytes = length(d[MIME"text/html"()]), + fallback = nothing, + )) + return rawhtml(d[MIME"text/html"()]) + elseif has_text_html && !isnothing(image) + # The 'text/html' representation of an @example block is above the threshold, + # falling back to '$(image.mime)' representation. + push!(dctx.ctx.atexample_warnings, AtExampleFallbackWarning( + page = dctx.navnode.page, + size_bytes = length(d[MIME"text/html"()]), + fallback = image.mime, + )) + return image.dom + elseif !has_text_html && !isnothing(image) + return image.dom + end + # Check for some 'fallback' MIMEs, defaulting to 'text/plain' if we can't find any of them. + return if haskey(d, MIME"text/latex"()) # If the show(io, ::MIME"text/latex", x) output is already wrapped in \[ ... \] or $$ ... $$, we # unwrap it first, since when we output Markdown.LaTeX objects we put the correct # delimiters around it anyway. @@ -2223,12 +2408,31 @@ function domify(dctx::DCtx, node::Node, d::Dict{MIME,Any}) elseif haskey(d, MIME"text/plain"()) @tags pre text = d[MIME"text/plain"()] - return pre[".documenter-example-output"](domify_ansicoloredtext(text, "nohighlight hljs")) + pre[".documenter-example-output"](domify_ansicoloredtext(text, "nohighlight hljs")) else error("this should never happen.") end end +function domify_show_image_binary(dctx::DCtx, filetype::AbstractString, d::Dict{MIME,Any}) + @tags img + mime_name = "image/$filetype" + mime = MIME{Symbol(mime_name)}() + # When we construct `d` in the expander pipeline, we call `stringmime`, which + # base64-encodes the bytes, so the values in the dictionary are base64-encoded. + # So if we do write it to a file, we need to decode it first. + data_base64 = d[mime] + filename = write_data_file(dctx, Base64.base64decode(data_base64); suffix=".$filetype") + alt = (:alt => "Example block output") + dom = if isnothing(filename) + src = string("data:$(mime_name);base64,", data_base64) + img[:src => src, alt] + else + img[:src => filename, alt] + end + (; dom, mime = mime_name) +end + # filehrefs # ------------------------------------------------------------------------------ diff --git a/test/examples/images/big.gif b/test/examples/images/big.gif new file mode 100644 index 0000000000..65e7f813b1 Binary files /dev/null and b/test/examples/images/big.gif differ diff --git a/test/examples/images/big.png b/test/examples/images/big.png new file mode 100644 index 0000000000..c7bab300d7 Binary files /dev/null and b/test/examples/images/big.png differ diff --git a/test/examples/images/big.svg b/test/examples/images/big.svg new file mode 100644 index 0000000000..7f22dcdae5 --- /dev/null +++ b/test/examples/images/big.svg @@ -0,0 +1,726 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/examples/images/big.webp b/test/examples/images/big.webp new file mode 100644 index 0000000000..696427a54f Binary files /dev/null and b/test/examples/images/big.webp differ diff --git a/test/examples/images/tiny.jpeg b/test/examples/images/tiny.jpeg new file mode 100644 index 0000000000..7a732a0088 Binary files /dev/null and b/test/examples/images/tiny.jpeg differ diff --git a/test/examples/images/tiny.png b/test/examples/images/tiny.png new file mode 100644 index 0000000000..ea062c1e1c Binary files /dev/null and b/test/examples/images/tiny.png differ diff --git a/test/examples/images/tiny.webp b/test/examples/images/tiny.webp new file mode 100644 index 0000000000..eaebe851fb Binary files /dev/null and b/test/examples/images/tiny.webp differ diff --git a/test/examples/make.jl b/test/examples/make.jl index 36bbdb867c..d574882063 100644 --- a/test/examples/make.jl +++ b/test/examples/make.jl @@ -1,3 +1,4 @@ +import SHA using Documenter include("../TestUtilities.jl"); using Main.TestUtilities @@ -142,6 +143,39 @@ module AutoDocs end end +struct MIMEBytes{M <: MIME} + bytes :: Vector{UInt8} + hash_slug :: String + function MIMEBytes(mime::AbstractString, bytes::AbstractVector{UInt8}) + hash_slug = bytes2hex(SHA.sha1(bytes))[1:8] + new{MIME{Symbol(mime)}}(bytes, hash_slug) + end +end +Base.show(io::IO, ::M, obj::MIMEBytes{M}) where {M <: MIME} = write(io, obj.bytes) + +const AT_EXAMPLE_FILES = Dict( + ("png", :big) => MIMEBytes("image/png", read(joinpath(@__DIR__, "images", "big.png"))), + ("png", :tiny) => MIMEBytes("image/png", read(joinpath(@__DIR__, "images", "tiny.png"))), + ("webp", :big) => MIMEBytes("image/webp", read(joinpath(@__DIR__, "images", "big.webp"))), + ("webp", :tiny) => MIMEBytes("image/webp", read(joinpath(@__DIR__, "images", "tiny.webp"))), + ("gif", :big) => MIMEBytes("image/gif", read(joinpath(@__DIR__, "images", "big.gif"))), + ("jpeg", :tiny) => MIMEBytes("image/jpeg", read(joinpath(@__DIR__, "images", "tiny.jpeg"))), +) +SVG_BIG = MIMEBytes("image/svg+xml", read(joinpath(@__DIR__, "images", "big.svg"))) +SVG_HTML = MIMEBytes("text/html", read(joinpath(@__DIR__, "images", "big.svg"))) + +struct MultiMIMESVG + bytes :: Vector{UInt8} + hash_slug :: String + function MultiMIMESVG(bytes::AbstractVector{UInt8}) + hash_slug = bytes2hex(SHA.sha1(bytes))[1:8] + new(bytes, hash_slug) + end +end +Base.show(io::IO, ::MIME"image/svg+xml", obj::MultiMIMESVG) = write(io, obj.bytes) +Base.show(io::IO, ::MIME"text/html", obj::MultiMIMESVG) = write(io, obj.bytes) +SVG_MULTI = MultiMIMESVG(read(joinpath(@__DIR__, "images", "big.svg"))) + # Helper functions function withassets(f, assets...) src(asset) = joinpath(@__DIR__, asset) @@ -211,6 +245,10 @@ htmlbuild_pages = Any[ "editurl/ugly.md", ], "xrefs.md", + "Outputs" => [ + "outputs/index.md", + "outputs/outputs.md", + ], ] function html_doc( diff --git a/test/examples/references/latex_showcase.tex b/test/examples/references/latex_showcase.tex index 57611537b6..2817f4dc9a 100644 --- a/test/examples/references/latex_showcase.tex +++ b/test/examples/references/latex_showcase.tex @@ -2,7 +2,7 @@ \newcommand{\DocMainTitle}{Documenter LaTeX Showcase} \newcommand{\DocVersion}{1.2.3} \newcommand{\DocAuthors}{} -\newcommand{\JuliaVersion}{1.9.2} +\newcommand{\JuliaVersion}{1.9.3} % ---- Insert preamble \input{preamble.tex} @@ -1161,7 +1161,7 @@ \chapter{Docstrings} -\href{https://example.org/Repository.jl/blob/make.jl#L27-31}{\texttt{source}} +\href{https://example.org/Repository.jl/blob/make.jl#L28-32}{\texttt{source}} \end{adjustwidth} @@ -1178,7 +1178,7 @@ \chapter{Docstrings} -\href{https://example.org/Repository.jl/blob/make.jl#L34-38}{\texttt{source}} +\href{https://example.org/Repository.jl/blob/make.jl#L35-39}{\texttt{source}} \end{adjustwidth} @@ -1252,7 +1252,7 @@ \section{An index of docstrings} -\href{https://example.org/Repository.jl/blob/make.jl#L41-71}{\texttt{source}} +\href{https://example.org/Repository.jl/blob/make.jl#L42-72}{\texttt{source}} \end{adjustwidth} @@ -1271,7 +1271,7 @@ \section{at-autodocs} -\href{https://example.org/Repository.jl/blob/make.jl#L75-75}{\texttt{source}} +\href{https://example.org/Repository.jl/blob/make.jl#L76-76}{\texttt{source}} \end{adjustwidth} @@ -1283,7 +1283,7 @@ \section{at-autodocs} -\href{https://example.org/Repository.jl/blob/make.jl#L88-88}{\texttt{source}} +\href{https://example.org/Repository.jl/blob/make.jl#L89-89}{\texttt{source}} \end{adjustwidth} @@ -1295,7 +1295,7 @@ \section{at-autodocs} -\href{https://example.org/Repository.jl/blob/make.jl#L91-91}{\texttt{source}} +\href{https://example.org/Repository.jl/blob/make.jl#L92-92}{\texttt{source}} \end{adjustwidth} @@ -1307,7 +1307,7 @@ \section{at-autodocs} -\href{https://example.org/Repository.jl/blob/make.jl#L85-85}{\texttt{source}} +\href{https://example.org/Repository.jl/blob/make.jl#L86-86}{\texttt{source}} \end{adjustwidth} @@ -1319,7 +1319,7 @@ \section{at-autodocs} -\href{https://example.org/Repository.jl/blob/make.jl#L94-94}{\texttt{source}} +\href{https://example.org/Repository.jl/blob/make.jl#L95-95}{\texttt{source}} \end{adjustwidth} diff --git a/test/examples/references/latex_simple.tex b/test/examples/references/latex_simple.tex index e28b9e7417..b4e38abed2 100644 --- a/test/examples/references/latex_simple.tex +++ b/test/examples/references/latex_simple.tex @@ -2,7 +2,7 @@ \newcommand{\DocMainTitle}{Documenter LaTeX Simple Non-Docker} \newcommand{\DocVersion}{1.2.3} \newcommand{\DocAuthors}{} -\newcommand{\JuliaVersion}{1.9.2} +\newcommand{\JuliaVersion}{1.9.3} % ---- Insert preamble \input{preamble.tex} diff --git a/test/examples/src/example-output.md b/test/examples/src/example-output.md index 8123a4fbef..40121eb112 100644 --- a/test/examples/src/example-output.md +++ b/test/examples/src/example-output.md @@ -9,3 +9,44 @@ Checking that `@example` output is contained in a specific HTML class. ```@example println("hello") ``` + +## `@example` outputs to file + +```@example +Main.AT_EXAMPLE_FILES[("png", :big)] +``` +```@example +Main.AT_EXAMPLE_FILES[("png", :tiny)] +``` +```@example +Main.AT_EXAMPLE_FILES[("webp", :big)] +``` +```@example +Main.AT_EXAMPLE_FILES[("webp", :tiny)] +``` +```@example +Main.AT_EXAMPLE_FILES[("gif", :big)] +``` +```@example +Main.AT_EXAMPLE_FILES[("jpeg", :tiny)] +``` + +### SVG output + +```@example +Main.SVG_BIG +``` + +### `text/html` fallbacks + +SVG with just `text/html` output (in practice, `DataFrame`s and such would fall into this category): + +```@example +Main.SVG_HTML +``` + +SVG with both `text/html` and `image/svg+xml` MIME, in which case we expect to pick the image one (because text is too big) and write it to a file. + +```@example +Main.SVG_MULTI +``` diff --git a/test/examples/src/index.md b/test/examples/src/index.md index f4ce44aae0..0a176cc20c 100644 --- a/test/examples/src/index.md +++ b/test/examples/src/index.md @@ -655,3 +655,24 @@ They do not get evaluated. !!! ukw "Unknown admonition class" Admonition with an unknown admonition class. + +## `@example` outputs to file + +```@example +Main.AT_EXAMPLE_FILES[("png", :big)] +``` +```@example +Main.AT_EXAMPLE_FILES[("png", :tiny)] +``` +```@example +Main.AT_EXAMPLE_FILES[("webp", :big)] +``` +```@example +Main.AT_EXAMPLE_FILES[("webp", :tiny)] +``` +```@example +Main.AT_EXAMPLE_FILES[("gif", :big)] +``` +```@example +Main.AT_EXAMPLE_FILES[("jpeg", :tiny)] +``` diff --git a/test/examples/src/outputs/index.md b/test/examples/src/outputs/index.md new file mode 100644 index 0000000000..f5dffd6821 --- /dev/null +++ b/test/examples/src/outputs/index.md @@ -0,0 +1,22 @@ +# Output handling (`outputs/index.md`) + +## `@example` outputs to file + +```@example +Main.AT_EXAMPLE_FILES[("png", :big)] +``` +```@example +Main.AT_EXAMPLE_FILES[("png", :tiny)] +``` +```@example +Main.AT_EXAMPLE_FILES[("webp", :big)] +``` +```@example +Main.AT_EXAMPLE_FILES[("webp", :tiny)] +``` +```@example +Main.AT_EXAMPLE_FILES[("gif", :big)] +``` +```@example +Main.AT_EXAMPLE_FILES[("jpeg", :tiny)] +``` diff --git a/test/examples/src/outputs/outputs.md b/test/examples/src/outputs/outputs.md new file mode 100644 index 0000000000..ff3673cb7e --- /dev/null +++ b/test/examples/src/outputs/outputs.md @@ -0,0 +1,22 @@ +# Output handling (`outputs/outputs.md`) + +## `@example` outputs to file + +```@example +Main.AT_EXAMPLE_FILES[("png", :big)] +``` +```@example +Main.AT_EXAMPLE_FILES[("png", :tiny)] +``` +```@example +Main.AT_EXAMPLE_FILES[("webp", :big)] +``` +```@example +Main.AT_EXAMPLE_FILES[("webp", :tiny)] +``` +```@example +Main.AT_EXAMPLE_FILES[("gif", :big)] +``` +```@example +Main.AT_EXAMPLE_FILES[("jpeg", :tiny)] +``` diff --git a/test/examples/tests.jl b/test/examples/tests.jl index 93fff83ca3..b01201f4b5 100644 --- a/test/examples/tests.jl +++ b/test/examples/tests.jl @@ -1,5 +1,6 @@ using Test import JSON +import Base64 # DOCUMENTER_TEST_EXAMPLES can be used to control which builds are performed in # make.jl. But for the tests we need to make sure that all the relevant builds @@ -76,7 +77,7 @@ all_md_files_in_src = let srcdir = joinpath(@__DIR__, "src"), mdfiles = String[] end mdfiles end -@test length(all_md_files_in_src) == 25 +@test length(all_md_files_in_src) == 27 @testset "Examples" begin @testset "HTML: deploy/$name" for (doc, name) in [ @@ -182,6 +183,37 @@ end @test haskey(siteinfo_json["documenter"], "julia_version") @test haskey(siteinfo_json["documenter"], "generation_timestamp") end + + @testset "at-example outputs: $fmt/$size" for ((fmt, size), data) in AT_EXAMPLE_FILES + if size === :tiny + encoded_data = Base64.base64encode(data.bytes) + @test occursin(encoded_data, read(joinpath(build_dir, "index.html"), String)) + @test occursin(encoded_data, read(joinpath(build_dir, "example-output", "index.html"), String)) + @test occursin(encoded_data, read(joinpath(build_dir, "outputs", "index.html"), String)) + @test occursin(encoded_data, read(joinpath(build_dir, "outputs", "outputs", "index.html"), String)) + else # size === :big + # From src/index.md + @test isfile(joinpath(build_dir, "index-$(data.hash_slug).$(fmt)")) + @test read(joinpath(build_dir, "index-$(data.hash_slug).$(fmt)")) == data.bytes + # From src/example-output.md + @test isfile(joinpath(build_dir, "example-output", "$(data.hash_slug).$(fmt)")) + @test read(joinpath(build_dir, "example-output", "$(data.hash_slug).$(fmt)")) == data.bytes + # From src/outputs/index.md + @test isfile(joinpath(build_dir, "outputs", "index-$(data.hash_slug).$(fmt)")) + @test read(joinpath(build_dir, "outputs", "index-$(data.hash_slug).$(fmt)")) == data.bytes + # From src/outputs/outputs.md + @test isfile(joinpath(build_dir, "outputs", "outputs", "$(data.hash_slug).$(fmt)")) + @test read(joinpath(build_dir, "outputs", "outputs", "$(data.hash_slug).$(fmt)")) == data.bytes + end + end + # SVG on src/example-output.md + @test isfile(joinpath(build_dir, "example-output", "$(SVG_BIG.hash_slug).svg")) + @test read(joinpath(build_dir, "example-output", "$(SVG_BIG.hash_slug).svg")) == SVG_BIG.bytes + # SVG on src/example-output.md, from Main.SVG_MULTI + @test isfile(joinpath(build_dir, "example-output", "$(SVG_BIG.hash_slug)-001.svg")) + @test read(joinpath(build_dir, "example-output", "$(SVG_BIG.hash_slug).svg")) == SVG_BIG.bytes + # .. but, crucially, Main.SVG_HTML did _not_ get written out. + @test !isfile(joinpath(build_dir, "example-output", "$(SVG_BIG.hash_slug)-002.svg")) end end @@ -209,6 +241,37 @@ end documenterjs = String(read(joinpath(build_dir, "assets", "documenter.js"))) @test occursin("languages/julia.min", documenterjs) @test occursin("languages/julia-repl.min", documenterjs) + + @testset "at-example outputs: $fmt/$size" for ((fmt, size), data) in AT_EXAMPLE_FILES + if size === :tiny + encoded_data = Base64.base64encode(data.bytes) + @test occursin(encoded_data, read(joinpath(build_dir, "index.html"), String)) + @test occursin(encoded_data, read(joinpath(build_dir, "example-output.html"), String)) + @test occursin(encoded_data, read(joinpath(build_dir, "outputs", "index.html"), String)) + @test occursin(encoded_data, read(joinpath(build_dir, "outputs", "outputs.html"), String)) + else # size === :big + # From src/index.md + @test isfile(joinpath(build_dir, "index-$(data.hash_slug).$(fmt)")) + @test read(joinpath(build_dir, "index-$(data.hash_slug).$(fmt)")) == data.bytes + # From src/example-output.md + @test isfile(joinpath(build_dir, "example-output-$(data.hash_slug).$(fmt)")) + @test read(joinpath(build_dir, "example-output-$(data.hash_slug).$(fmt)")) == data.bytes + # From src/outputs/index.md + @test isfile(joinpath(build_dir, "outputs", "index-$(data.hash_slug).$(fmt)")) + @test read(joinpath(build_dir, "outputs", "index-$(data.hash_slug).$(fmt)")) == data.bytes + # From src/outputs/outputs.md + @test isfile(joinpath(build_dir, "outputs", "outputs-$(data.hash_slug).$(fmt)")) + @test read(joinpath(build_dir, "outputs", "outputs-$(data.hash_slug).$(fmt)")) == data.bytes + end + end + # SVG on src/example-output.md + @test isfile(joinpath(build_dir, "example-output-$(SVG_BIG.hash_slug).svg")) + @test read(joinpath(build_dir, "example-output-$(SVG_BIG.hash_slug).svg")) == SVG_BIG.bytes + # SVG on src/example-output.md, from Main.SVG_MULTI + @test isfile(joinpath(build_dir, "example-output-$(SVG_BIG.hash_slug)-001.svg")) + @test read(joinpath(build_dir, "example-output-$(SVG_BIG.hash_slug).svg")) == SVG_BIG.bytes + # .. but, crucially, Main.SVG_HTML did _not_ get written out. + @test !isfile(joinpath(build_dir, "example-output-$(SVG_BIG.hash_slug)-002.svg")) end end