Skip to content

Commit

Permalink
User API for non-local assets (#1108)
Browse files Browse the repository at this point in the history
  • Loading branch information
mortenpi authored Aug 25, 2019
1 parent 78172a7 commit 5ae0677
Show file tree
Hide file tree
Showing 8 changed files with 181 additions and 52 deletions.
1 change: 1 addition & 0 deletions docs/src/lib/public.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Pages = ["public.md"]
Documenter
makedocs
hide
asset
deploydocs
Deps
Deps.pip
Expand Down
4 changes: 2 additions & 2 deletions src/Documenter.jl
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ include("Writers/Writers.jl")
include("Deps.jl")

import .Utilities: Selectors
import .Writers.HTMLWriter: HTML
import .Writers.HTMLWriter: HTML, asset
import .Writers.HTMLWriter.JS: KaTeX, MathJax

# User Interface.
# ---------------
export Deps, makedocs, deploydocs, hide, doctest, DocMeta, KaTeX, MathJax
export Deps, makedocs, deploydocs, hide, doctest, DocMeta, KaTeX, MathJax, asset

"""
makedocs(
Expand Down
119 changes: 98 additions & 21 deletions src/Writers/HTMLWriter.jl
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ import ...Documenter:
Expanders,
Documenter,
Utilities,
Writers
Writers,
asset

using ...Utilities.JSDependencies: JSDependencies, json_jsescape
import ...Utilities.DOM: DOM, Tag, @tags
Expand All @@ -68,6 +69,74 @@ const ASSETS_SASS = joinpath(ASSETS, "scss")
"Directory for the compiled CSS files of the themes."
const ASSETS_THEMES = joinpath(ASSETS, "themes")

struct HTMLAsset
class :: Symbol
uri :: String
islocal :: Bool

function HTMLAsset(class::Symbol, uri::String, islocal::Bool)
if !islocal && match(r"^https?://", uri) === nothing
error("Remote asset URL must start with http:// or https://")
end
class in [:ico, :css, :js] || error("Unrecognised asset class $class for `$(uri)`")
new(class, uri, islocal)
end
end

"""
asset(uri)
Can be used to pass non-local web assets to [`HTML`](@ref), where `uri` should be an absolute
HTTP or HTTPS URL.
It accepts the following keyword arguments:
**`class`** can be used to override the asset class, which determines how exactly the asset
gets included in the HTML page. This is necessary if the class can not be determined
automatically (default).
Should be one of: `:js`, `:css` or `:ico`. They become a `<script>`,
`<link rel="stylesheet" type="text/css">` and `<link rel="icon" type="image/x-icon">`
elements in `<head>`, respectively.
**`islocal`** can be used to declare the asset to be local. The `uri` should then be a path
relative to the documentation source directory (conventionally `src/`). This can be useful
when it is necessary to override the asset class of a local asset.
# Usage
```julia
Documenter.HTML(assets = [
# Standard local asset
"assets/extra_styles.css",
# Standard remote asset (extension used to determine that class = :js)
asset("https://example.com/jslibrary.js"),
# Setting asset class manually, since it can't be determined manually
asset("https://example.com/fonts", class = :css),
# Same as above, but for a local asset
asset("asset/foo.script", class=:js, islocal=true),
])
```
"""
function asset(uri; class = nothing, islocal=false)
if class === nothing
class = assetclass(uri)
(class === nothing) && error("""
Unable to determine asset class for: $(uri)
It can be set explicitly with the `class` keyword argument.
""")
end
HTMLAsset(class, uri, islocal)
end

function assetclass(uri)
# TODO: support actual proper URIs
ext = splitext(uri)[end]
ext == ".ico" ? :ico :
ext == ".css" ? :css :
ext == ".js" ? :js : :unknown
end

abstract type MathEngine end

"""
Expand Down Expand Up @@ -229,19 +298,24 @@ in this order. The first one it finds gets displayed at the top of the navigatio
It will also check for `assets/logo-dark.{svg,png,webp,gif,jpg,jpeg}` and use that for dark
themes.
Additional JS, ICO, and CSS assets can be included in the generated pages using the
`assets` keyword for `makedocs`. `assets` must be a `Vector{String}` and will include
each listed asset in the `<head>` of every page in the order in which they are listed.
The type of the asset (i.e. whether it is going to be included with a `<script>` or a
`<link>` tag) is determined by the file's extension -- either `.js`, `.ico`, or `.css`.
Adding an ICO asset is primarilly useful for setting a custom `favicon`.
Additional JS, ICO, and CSS assets can be included in the generated pages by passing them as
a list with the `assets` keyword. Each asset will be included in the `<head>` of every page
in the order in which they are given. The type of the asset (i.e. whether it is going to be
included with a `<script>` or a `<link>` tag) is determined by the file's extension --
either `.js`, `.ico`[^1], or `.css` (unless overridden with [`asset`](@ref)).
Simple strings are assumed to be local assets and that each correspond to a file relative to
the documentation source directory (conventionally `src/`). Non-local assets, identified by
their absolute URLs, can be included with the [`asset`](@ref) function.
[^1]: Adding an ICO asset is primarily useful for setting a custom `favicon`.
"""
struct HTML <: Documenter.Writer
prettyurls :: Bool
disable_git :: Bool
edit_branch :: Union{String, Nothing}
canonical :: Union{String, Nothing}
assets :: Vector{String}
assets :: Vector{HTMLAsset}
analytics :: String
collapselevel :: Int
sidebar_sitename :: Bool
Expand All @@ -253,14 +327,19 @@ struct HTML <: Documenter.Writer
disable_git :: Bool = false,
edit_branch :: Union{String, Nothing} = "master",
canonical :: Union{String, Nothing} = nothing,
assets :: Vector{String} = String[],
assets :: Vector = String[],
analytics :: String = "",
collapselevel :: Integer = 2,
sidebar_sitename :: Bool = true,
highlights :: Vector{String} = String[],
mathengine :: Union{MathEngine,Nothing} = KaTeX(),
)
collapselevel >= 1 || thrown(ArgumentError("collapselevel must be >= 1"))
collapselevel >= 1 || throw(ArgumentError("collapselevel must be >= 1"))
assets = map(assets) do asset
isa(asset, HTMLAsset) && return asset
isa(asset, AbstractString) && return HTMLAsset(assetclass(asset), asset, true)
error("Invalid value in assets: $(asset) [$(typeof(asset))]")
end
new(prettyurls, disable_git, edit_branch, canonical, assets, analytics,
collapselevel, sidebar_sitename, highlights, mathengine)
end
Expand Down Expand Up @@ -385,11 +464,10 @@ mutable struct HTMLContext
search_index :: Vector{SearchRecord}
search_index_js :: String
search_navnode :: Documents.NavNode
local_assets :: Vector{String}
footnotes :: Vector{Markdown.Footnote}
end

HTMLContext(doc, settings=HTML()) = HTMLContext(doc, settings, [], "", "", "", [], "", Documents.NavNode("search", "Search", nothing), [], [])
HTMLContext(doc, settings=HTML()) = HTMLContext(doc, settings, [], "", "", "", [], "", Documents.NavNode("search", "Search", nothing), [])

function SearchRecord(ctx::HTMLContext, navnode; loc="", title=nothing, category="page", text="")
page_title = mdflatten(pagetitle(ctx, navnode))
Expand Down Expand Up @@ -480,7 +558,6 @@ function render(doc::Documents.Document, settings::HTML=HTML())
for theme in THEMES
copy_asset("themes/$(theme).css", doc)
end
append!(ctx.local_assets, settings.assets)

for page in keys(doc.blueprint.pages)
idx = findfirst(nn -> nn.page == page, doc.internal.navlist)
Expand Down Expand Up @@ -666,7 +743,7 @@ function render_head(ctx, navnode)
script[:src => relhref(src, "../versions.js")],

# Custom user-provided assets.
asset_links(src, ctx.local_assets),
asset_links(src, ctx.settings.assets),
# Themes. Note: we reverse the make sure that the default theme (first in the array)
# comes as the last <link> tag.
map(Iterators.reverse(enumerate(THEMES))) do (i, theme)
Expand All @@ -682,16 +759,16 @@ function render_head(ctx, navnode)
)
end

function asset_links(src::AbstractString, assets::Vector)
function asset_links(src::AbstractString, assets::Vector{HTMLAsset})
@tags link script
links = DOM.Node[]
for each in assets
ext = splitext(each)[end]
url = relhref(src, each)
for asset in assets
class = asset.class
url = asset.islocal ? relhref(src, asset.uri) : asset.uri
node =
ext == ".ico" ? link[:href => url, :rel => "icon", :type => "image/x-icon"] :
ext == ".css" ? link[:href => url, :rel => "stylesheet", :type => "text/css"] :
ext == ".js" ? script[:src => url] : continue # Skip non-js/css files.
class == :ico ? link[:href => url, :rel => "icon", :type => "image/x-icon"] :
class == :css ? link[:href => url, :rel => "stylesheet", :type => "text/css"] :
class == :js ? script[:src => url] : continue # Skip non-js/css files.
push!(links, node)
end
return links
Expand Down
19 changes: 14 additions & 5 deletions test/TestUtilities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,35 @@ using Documenter.Utilities: withoutput

export @quietly

struct QuietlyException <: Exception
exception
backtrace
end

function Base.showerror(io::IO, e::QuietlyException)
println(io, "@quietly hit an exception ($(typeof(e.exception))):")
showerror(io, e.exception, e.backtrace)
end

function _quietly(f, expr, source)
result, success, backtrace, output = withoutput(f)
if success
printstyled("@quietly: success, $(sizeof(output)) bytes of output hidden\n"; color=:magenta)
return result
else
@error """
An error was thrown in @quietly
Error: $(repr(result)) at $(source.file):$(source.line)
Expression:
An error was thrown in @quietly, $(sizeof(output)) bytes of output captured
$(typeof(result)) at $(source.file):$(source.line) in expression:
$(expr)
$(sizeof(output)) bytes of output captured
"""
if length(output) > 0
if !isempty(output)
printstyled("$("="^21) @quietly: output from the expression $("="^21)\n"; color=:magenta)
print(output)
last(output) != "\n" && println()
printstyled("$("="^27) @quietly: end of output $("="^28)\n"; color=:magenta)
end
throw(result)
throw(QuietlyException(result, backtrace))
end
end
macro quietly(expr)
Expand Down
16 changes: 9 additions & 7 deletions test/examples/make.jl
Original file line number Diff line number Diff line change
Expand Up @@ -172,16 +172,15 @@ function withassets(f, assets...)
for asset in assets
cp(src(asset), dst(asset))
end
rv = try
f()
catch exception
@warn "f() threw an exception" exception
nothing
rv, exception = try
f(), nothing
catch e
nothing, e
end
for asset in assets
rm(dst(asset))
end
return rv
return (exception === nothing) ? rv : throw(exception)
end

examples_html_deploy_doc = @quietly withassets("images/logo.png", "images/logo.jpg", "images/logo.gif") do
Expand All @@ -197,7 +196,10 @@ examples_html_deploy_doc = @quietly withassets("images/logo.png", "images/logo.j
format = Documenter.HTML(
assets = [
"assets/favicon.ico",
"assets/custom.css"
"assets/custom.css",
asset("https://example.com/resource.js"),
asset("http://example.com/fonts?param=foo", class=:css),
asset("https://fonts.googleapis.com/css?family=Nanum+Brush+Script&display=swap", class=:css),
],
prettyurls = true,
canonical = "https://example.com/stable",
Expand Down
19 changes: 18 additions & 1 deletion test/examples/src/man/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,7 @@ With explicit alignment.
| `A` || 10.00 |
| `BB` || 1000000.00 |

## Customizing MathJax
## Customizing assets

The following example only works on the deploy build where

Expand All @@ -368,3 +368,20 @@ mathengine = Documenter.MathJax(Dict(:TeX => Dict(
```math
\bra{x}\ket{y}
```

The following at-raw block only renders correctly on the deploy build, where

```julia
assets = [
asset("https://fonts.googleapis.com/css?family=Nanum+Brush+Script&display=swap", class=:css),
...
]
```

```@raw html
<div style="font-family: 'Nanum Brush Script'; text-align: center; font-size: xx-large;">
Hello World!
<br />
Written in <a href="https://fonts.google.com/specimen/Nanum+Brush+Script">Nanum Brush Script.</a>
</div>
```
8 changes: 4 additions & 4 deletions test/examples/tests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,6 @@ end

@test isa(doc, Documenter.Documents.Document)

# TODO: test the HTML build

let build_dir = joinpath(examples_root, "builds", "html-local")

index_html = read(joinpath(build_dir, "index.html"), String)
Expand All @@ -105,14 +103,16 @@ end

@test isa(doc, Documenter.Documents.Document)

# TODO: test the HTML build with pretty URLs

let build_dir = joinpath(examples_root, "builds", "html-deploy")
@test joinpath(build_dir, "index.html") |> isfile
@test joinpath(build_dir, "omitted", "index.html") |> isfile
@test joinpath(build_dir, "hidden", "index.html") |> isfile
@test joinpath(build_dir, "lib", "autodocs", "index.html") |> isfile

# Test existence of some HTML elements
indexhtml = String(read(joinpath(build_dir, "index.html")))
#@test occursin("", indexhtml)

# Assets
@test joinpath(build_dir, "assets", "documenter.js") |> isfile

Expand Down
Loading

0 comments on commit 5ae0677

Please sign in to comment.