diff --git a/src/html/HTMLWriter.jl b/src/html/HTMLWriter.jl
index bd15fffb53..5a00283ee0 100644
--- a/src/html/HTMLWriter.jl
+++ b/src/html/HTMLWriter.jl
@@ -484,6 +484,7 @@ struct HTML <: Documenter.Writer
size_threshold_ignore :: Vector{String}
example_size_threshold :: Int
inventory_version :: Union{String,Nothing}
+ offline_version :: Bool
function HTML(;
prettyurls :: Bool = true,
@@ -513,6 +514,7 @@ struct HTML <: Documenter.Writer
# and leaves a buffer before hitting `size_threshold_warn`.
example_size_threshold :: Union{Integer, Nothing} = 8 * 2^10, # 8 KiB
inventory_version = nothing,
+ offline_version = false,
# deprecated keywords
edit_branch :: Union{String, Nothing, Default} = Default(nothing),
@@ -568,7 +570,7 @@ struct HTML <: Documenter.Writer
collapselevel, sidebar_sitename, highlights, mathengine, description, footer,
ansicolor, lang, warn_outdated, prerender, node, highlightjs,
size_threshold, size_threshold_warn, size_threshold_ignore, example_size_threshold,
- (isnothing(inventory_version) ? nothing : string(inventory_version))
+ (isnothing(inventory_version) ? nothing : string(inventory_version)), offline_version
)
end
end
@@ -596,7 +598,7 @@ function prepare_prerendering(prerender, node, highlightjs, highlights)
end
@debug "HTMLWriter: downloading highlightjs"
r = Documenter.JSDependencies.RequireJS([])
- RD.highlightjs!(r, highlights)
+ RD.highlightjs!(r, false, "", "", highlights)
libs = sort!(collect(r.libraries); by = first) # puts highlight first
key = join((x.first for x in libs), ',')
highlightjs = get!(HLJSFILES, key) do
@@ -749,12 +751,15 @@ function render(doc::Documenter.Document, settings::HTML=HTML())
if isfile(joinpath(doc.user.source, "assets", "documenter.js"))
@warn "not creating 'documenter.js', provided by the user."
else
- r = JSDependencies.RequireJS([
- RD.jquery, RD.jqueryui, RD.headroom, RD.headroom_jquery,
- ])
- RD.mathengine!(r, settings.mathengine)
+ r = JSDependencies.RequireJS([RD.process_remote(url, settings.offline_version, joinpath(doc.user.build, "assets", "cdn"), joinpath(doc.user.build, "assets")) for url in [
+ RD.jquery,
+ RD.jqueryui,
+ RD.headroom,
+ RD.headroom_jquery,
+ ]])
+ RD.mathengine!(r, settings.mathengine, settings.offline_version, joinpath(doc.user.build, "assets", "cdn"), joinpath(doc.user.build, "assets"))
if !settings.prerender
- RD.highlightjs!(r, settings.highlights)
+ RD.highlightjs!(r, settings.offline_version, joinpath(doc.user.build, "assets", "cdn"), joinpath(doc.user.build, "assets"), settings.highlights)
end
for filename in readdir(joinpath(ASSETS, "js"))
path = joinpath(ASSETS, "js", filename)
@@ -952,12 +957,12 @@ function render_head(ctx, navnode)
default_site_description(ctx)
end
- css_links = [
+ css_links = [RD.process_remote(url, ctx.settings.offline_version, joinpath(ctx.doc.user.build, "assets", "cdn"), ctx.doc.user.build) for url in [
RD.lato,
RD.juliamono,
RD.fontawesome_css...,
RD.katex_css,
- ]
+ ]]
head(
meta[:charset=>"UTF-8"],
@@ -991,7 +996,7 @@ function render_head(ctx, navnode)
script("documenterBaseURL=\"$(relhref(src, "."))\""),
script[
- :src => RD.requirejs_cdn,
+ :src => RD.process_remote(RD.requirejs_cdn, ctx.settings.offline_version, joinpath(ctx.doc.user.build, "assets", "cdn"), ctx.doc.user.build),
Symbol("data-main") => relhref(src, ctx.documenter_js)
],
script[:src => relhref(src, ctx.search_index_js)],
diff --git a/src/html/RD.jl b/src/html/RD.jl
index a75f170352..102bc577ba 100644
--- a/src/html/RD.jl
+++ b/src/html/RD.jl
@@ -1,6 +1,7 @@
"Provides a namespace for remote dependencies."
module RD
using JSON: JSON
+ using Base64
using ....Documenter.JSDependencies: RemoteLibrary, Snippet, RequireJS, jsescape, json_jsescape
using ..HTMLWriter: KaTeX, MathJax, MathJax2, MathJax3
@@ -29,23 +30,23 @@ module RD
# highlight.js
"Add the highlight.js dependencies and snippet to a [`RequireJS`](@ref) declaration."
- function highlightjs!(r::RequireJS, languages = String[])
+ function highlightjs!(r::RequireJS, offline_version::Bool, build_path::AbstractString, origin_path=build_path, languages = String[])
# NOTE: the CSS themes for hightlightjs are compiled into the Documenter CSS
# When updating this dependency, it is also necessary to update the the CSS
# files the CSS files in assets/html/scss/highlightjs
hljs_version = "11.8.0"
- push!(r, RemoteLibrary(
+ push!(r, process_remote(RemoteLibrary(
"highlight",
"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/$(hljs_version)/highlight.min.js"
- ))
+ ), offline_version, build_path, origin_path))
languages = ["julia", "julia-repl", languages...]
for language in languages
language = jsescape(language)
- push!(r, RemoteLibrary(
+ push!(r, process_remote(RemoteLibrary(
"highlight-$(language)",
"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/$(hljs_version)/languages/$(language).min.js",
deps = ["highlight"]
- ))
+ ), offline_version, build_path, origin_path))
end
push!(r, Snippet(
vcat(["jquery", "highlight"], ["highlight-$(jsescape(language))" for language in languages]),
@@ -61,16 +62,16 @@ module RD
# MathJax & KaTeX
const katex_version = "0.16.8"
const katex_css = "https://cdnjs.cloudflare.com/ajax/libs/KaTeX/$(katex_version)/katex.min.css"
- function mathengine!(r::RequireJS, engine::KaTeX)
- push!(r, RemoteLibrary(
+ function mathengine!(r::RequireJS, engine::KaTeX, offline_version::Bool, build_path, origin_path=build_path)
+ push!(r, process_remote(RemoteLibrary(
"katex",
"https://cdnjs.cloudflare.com/ajax/libs/KaTeX/$(katex_version)/katex.min.js"
- ))
- push!(r, RemoteLibrary(
+ ), offline_version, build_path, origin_path))
+ push!(r, process_remote(RemoteLibrary(
"katex-auto-render",
"https://cdnjs.cloudflare.com/ajax/libs/KaTeX/$(katex_version)/contrib/auto-render.min.js",
deps = ["katex"],
- ))
+ ), offline_version, build_path, origin_path))
push!(r, Snippet(
["jquery", "katex", "katex-auto-render"],
["\$", "katex", "renderMathInElement"],
@@ -84,8 +85,9 @@ module RD
"""
))
end
- function mathengine!(r::RequireJS, engine::MathJax2)
+ function mathengine!(r::RequireJS, engine::MathJax2, offline_version::Bool, build_path, origin_path=build_path)
url = isempty(engine.url) ? "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.9/MathJax.js?config=TeX-AMS_HTML" : engine.url
+ url = process_remote(url, offline_version, build_path, origin_path)
push!(r, RemoteLibrary(
"mathjax",
url,
@@ -97,8 +99,9 @@ module RD
"""
))
end
- function mathengine!(r::RequireJS, engine::MathJax3)
+ function mathengine!(r::RequireJS, engine::MathJax3, offline_version::Bool, build_path, origin_path=build_path)
url = isempty(engine.url) ? "https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.2/es5/tex-svg.js" : engine.url
+ url = process_remote(url, offline_version, build_path, origin_path)
push!(r, Snippet([], [],
"""
window.MathJax = $(json_jsescape(engine.config, 2));
@@ -113,4 +116,51 @@ module RD
))
end
mathengine(::RequireJS, ::Nothing) = nothing
+
+ process_remote(dep, offline_version::Bool, build_path, origin_path=build_path) = offline_version ? _process(dep, build_path, origin_path) : dep
+ _process(dep::RemoteLibrary, build_path, origin_path) = RemoteLibrary(dep.name, _process(dep.url, build_path, origin_path); deps = dep.deps, exports = dep.exports)
+
+ _download_file_content(url::AbstractString) = String(take!(Downloads.download(url, output = IOBuffer())))
+
+ function _process(url::AbstractString, output_path, origin_path)
+ result = _download_file_content(url)
+ filename = split(url, "/")[end]
+ filepath = joinpath(output_path, filename)
+ if !isfile(filepath)
+ mkpath(dirname(filepath))
+ open(filepath, "w") do f
+ if splitext(filepath)[end] == ".css"
+ result = _process_downloaded_css(result, url)
+ end
+ write(f, result)
+ end
+ end
+
+ return relpath(filepath, origin_path*"/")
+ end
+
+ const font_ext_to_type = Dict(
+ ".ttf" => "truetype",
+ ".eot" => "embedded-opentype",
+ ".eot?#iefix" => "embedded-opentype",
+ ".svg#webfont" => "svg",
+ ".woff" => "woff",
+ ".woff2" => "woff2",
+ )
+
+ """
+ _process_downloaded_css(file_content, origin_url)
+
+ Process the downloaded file content of a CSS file. This detects the font URLs inside the file with a REGEX, downloads those fonts and replace the reference to the URL in the file with the content of the font file base64 encoded.
+ """
+ function _process_downloaded_css(file_content, origin_url)
+ url_regex = r"url\(([^)]+)\)"
+ replace(file_content, url_regex => s -> begin
+ rel_url = match(url_regex, s).captures[1] # Get the URL written in the content file
+ url = normpath(dirname(origin_url), rel_url) # Transform that relative URL into an absolute one for download
+ font_type = font_ext_to_type[splitext(rel_url)[end]] # Find the font type to put in the CSS file next to the encoded file, based on the file extension
+ encoded_file = Base64.base64encode(_download_file_content(url)) # Encode the file in base64
+ return "url(data:font/$(font_type);charset=utf-8;base64,$(encoded_file))" # Replace the whole url entry with the base64 encoding
+ end)
+ end
end
diff --git a/test/examples/make.jl b/test/examples/make.jl
index c68ac357cb..bcb26e8efc 100644
--- a/test/examples/make.jl
+++ b/test/examples/make.jl
@@ -18,7 +18,7 @@ EXAMPLE_BUILDS = if haskey(ENV, "DOCUMENTER_TEST_EXAMPLES")
split(ENV["DOCUMENTER_TEST_EXAMPLES"])
else
["html", "html-meta-custom", "html-mathjax2-custom", "html-mathjax3", "html-mathjax3-custom",
- "html-local", "html-draft", "html-repo-git", "html-repo-nothing", "html-repo-error",
+ "html-local", "html-offline", "html-draft", "html-repo-git", "html-repo-nothing", "html-repo-error",
"html-sizethreshold-defaults-fail", "html-sizethreshold-success", "html-sizethreshold-ignore-success", "html-sizethreshold-override-fail", "html-sizethreshold-ignore-success", "html-sizethreshold-ignore-fail",
"latex_texonly", "latex_simple_texonly", "latex_showcase_texonly", "html-pagesonly"]
end
@@ -476,6 +476,36 @@ else
nothing
end
+# HTML: offline_version
+examples_html_offline_doc = if "html-offline" in EXAMPLE_BUILDS
+ @info("Building mock package docs: HTMLWriter / offline build")
+ @quietly makedocs(
+ debug = true,
+ root = examples_root,
+ build = "builds/html-offline",
+ doctestfilters = [r"Ptr{0x[0-9]+}"],
+ sitename = "Documenter example",
+ pages = htmlbuild_pages,
+ expandfirst = expandfirst,
+ repo = "https://dev.azure.com/org/project/_git/repo?path={path}&version={commit}{line}&lineStartColumn=1&lineEndColumn=1",
+ linkcheck = true,
+ linkcheck_ignore = [r"(x|y).md", "z.md", r":func:.*"],
+ format = Documenter.HTML(
+ assets = [
+ "assets/custom.css"
+ ],
+ offline_version = true,
+ footer = nothing,
+ ),
+ # TODO: example_block failure only happens on windows, so that's not actually expected
+ warnonly = [:doctest, :footnote, :cross_references, :linkcheck, :example_block, :eval_block],
+ )
+else
+ @info "Skipping build: HTML/offline"
+ @debug "Controlling variables:" EXAMPLE_BUILDS get(ENV, "DOCUMENTER_TEST_EXAMPLES", nothing)
+ nothing
+end
+
# HTML: A few simple builds testing the repo keyword fallbacks
macro examplebuild(name, block)
docvar = Symbol("examples_html_", replace(name, "-" => "_"), "_doc")
diff --git a/test/examples/tests.jl b/test/examples/tests.jl
index a5c6335e38..45b87f0404 100644
--- a/test/examples/tests.jl
+++ b/test/examples/tests.jl
@@ -423,6 +423,25 @@ end
end
end
+
+ @testset "HTML: offline" begin
+ doc = Main.examples_html_offline_doc
+
+ @test isa(doc, Documenter.Documenter.Document)
+
+ let build_dir = joinpath(examples_root, "builds", "html-offline")
+
+ index_html = read(joinpath(build_dir, "index.html"), String)
+ @test occursin("", index_html)
+
+ # Assets
+ @test joinpath(build_dir, "assets", "documenter.js") |> isfile
+ @test joinpath(build_dir, "assets", "cdn", "lato-font.min.css") |> isfile
+ documenterjs = String(read(joinpath(build_dir, "assets", "documenter.js")))
+ @test occursin("'jquery': 'cdn/jquery.min'", documenterjs)
+ end
+ end
+
@testset "HTML: pagesonly" begin
doc = Main.examples_html_pagesonly_doc