Skip to content

Commit 2e0d3ef

Browse files
authored
Add inventory writing (#249)
1 parent c2fc02e commit 2e0d3ef

File tree

2 files changed

+133
-7
lines changed

2 files changed

+133
-7
lines changed

Project.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,24 @@ version = "0.1.12"
66
[deps]
77
ANSIColoredPrinters = "a4c015fc-c6ff-483c-b24f-f7ea428134e9"
88
Base64 = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f"
9+
DocInventories = "43dc2714-ed3b-44b5-b226-857eda1aa7de"
910
DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
1011
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
1112
IOCapture = "b5f81e59-6552-4d32-b1f0-c071b021bf89"
1213
Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a"
1314
NodeJS_20_jll = "c7aee132-11e1-519c-8219-0a43005e73c2"
1415
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
16+
TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
1517

1618
[compat]
1719
ANSIColoredPrinters = "0.0.1"
20+
DocInventories = "1.0"
1821
DocStringExtensions = "0.9"
1922
Documenter = "1"
2023
DocumenterCitations = "1"
2124
IOCapture = "0.2"
2225
NodeJS_20_jll = "20"
26+
TOML = "1.0.3"
2327
julia = "1.6"
2428

2529
[weakdeps]

src/writer.jl

Lines changed: 129 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import Documenter: Documenter, Builder, Expanders, MarkdownAST
22
import Documenter.DOM: escapehtml
3+
using DocInventories: DocInventories, Inventory, InventoryItem
4+
using TOML: TOML
35

46
import ANSIColoredPrinters
57
using Base64: base64decode, base64encode
@@ -68,6 +70,8 @@ Base.@kwdef struct MarkdownVitepress <: Documenter.Writer
6870
deploy_decision::Union{Nothing, Documenter.DeployDecision} = nothing
6971
"A list of assets, the same as what is provided to Documenter's HTMLWriter."
7072
assets = nothing
73+
"A version string to write to the header of the objects.inv inventory file. This should be a valid version number without a v prefix. Defaults to the version defined in the Project.toml file in the parent folder of the documentation root"
74+
inventory_version::Union{String,Nothing} = nothing
7175
end
7276

7377
# return the same file with the extension changed to .md
@@ -181,8 +185,8 @@ function render(doc::Documenter.Document, settings::MarkdownVitepress=MarkdownVi
181185
end
182186
# Documenter.jl wants assets in `assets/`, but Vitepress likes them in `public/`,
183187
# so we rename the folder.
188+
mkpath(joinpath(builddir, settings.md_output_path, "public"))
184189
if isdir(joinpath(sourcedir, "assets")) && !isdir(joinpath(sourcedir, "public"))
185-
mkpath(joinpath(builddir, settings.md_output_path, "public"))
186190
files = readdir(joinpath(builddir, settings.md_output_path, "assets"); join = true)
187191
logo_files = contains.(last.(splitdir.(files)), "logo")
188192
favicon_files = contains.(last.(splitdir.(files)), "favicon")
@@ -211,14 +215,30 @@ function render(doc::Documenter.Document, settings::MarkdownVitepress=MarkdownVi
211215
# This needs to be run after favicons and logos are moved to the public subfolder
212216
modify_config_file(doc, settings, deploy_decision)
213217

218+
version = settings.inventory_version
219+
if isnothing(version)
220+
project_toml = joinpath(dirname(doc.user.root), "Project.toml")
221+
version = _get_inventory_version(project_toml)
222+
end
223+
inventory = Inventory(; project=doc.user.sitename, version)
224+
214225
# Iterate over the pages, render each page separately
215226
for (src, page) in doc.blueprint.pages
216227
# This is where you can operate on a per-page level.
217228
open(docpath(page.build, builddir, settings.md_output_path), "w") do io
218229
for node in page.mdast.children
219-
render(io, mime, node, page, doc)
230+
render(io, mime, node, page, doc; inventory)
220231
end
221232
end
233+
item = InventoryItem(
234+
name = replace(splitext(src)[1], "\\" => "/"),
235+
domain = "std",
236+
role = "doc",
237+
dispname = _pagetitle(page),
238+
priority = -1,
239+
uri = _get_inventory_uri(doc, page, nothing)
240+
)
241+
push!(inventory, item)
222242
end
223243

224244
mkpath(joinpath(builddir, "final_site"))
@@ -228,6 +248,9 @@ function render(doc::Documenter.Document, settings::MarkdownVitepress=MarkdownVi
228248
touch(config_path)
229249
end
230250

251+
objects_inv = joinpath(builddir, settings.md_output_path, "public", "objects.inv")
252+
DocInventories.save(objects_inv, inventory)
253+
231254
# Now that the Markdown files are written, we can build the Vitepress site if required.
232255
if settings.build_vitepress
233256
@info "DocumenterVitepress: building Vitepress site."
@@ -352,7 +375,8 @@ function render(io::IO, mime::MIME"text/plain", vec::Vector, page, doc; kwargs..
352375
end
353376

354377
function render(io::IO, mime::MIME"text/plain", node::Documenter.MarkdownAST.Node, anchor::Documenter.Anchor, page, doc; kwargs...)
355-
println(io, "\n<a id='", lstrip(Documenter.anchor_fragment(anchor), '#'), "'></a>")
378+
anchor_id = lstrip(Documenter.anchor_fragment(anchor), '#')
379+
println(io, "\n<a id='", anchor_id, "'></a>")
356380
return render(io, mime, node, anchor.object, page, doc; kwargs...)
357381
end
358382

@@ -381,8 +405,17 @@ end
381405
function render(io::IO, mime::MIME"text/plain", node::Documenter.MarkdownAST.Node, docs::Documenter.DocsNode, page, doc; kwargs...)
382406
open_txt = get(page.globals.meta, :CollapsedDocStrings, false) ? "" : "open"
383407
anchor_id = sanitized_anchor_label(docs.anchor)
408+
category = Documenter.doccat(docs.object)
409+
if haskey(kwargs, :inventory)
410+
item = InventoryItem(
411+
name = docs.anchor.id,
412+
role = lowercase(category),
413+
uri = _get_inventory_uri(doc, page, anchor_id),
414+
)
415+
push!(kwargs[:inventory], item)
416+
end
384417
# Docstring header based on the name of the binding and it's category.
385-
_badge_text = """<Badge type="info" class="jlObjectType jl$(Documenter.doccat(docs.object))" text="$(Documenter.doccat(docs.object))" />"""
418+
_badge_text = """<Badge type="info" class="jlObjectType jl$(category)" text="$(category)" />"""
386419
print(io ,"""<details class='jldocstring custom-block' $(open_txt)>
387420
<summary><a id='$(anchor_id)' href='#$(anchor_id)'><span class="jlbinding">$(docs.object.binding)</span></a> $(_badge_text)</summary>\n
388421
""")
@@ -892,16 +925,25 @@ end
892925
# Headers
893926
function render(io::IO, mime::MIME"text/plain", node::Documenter.MarkdownAST.Node, header::Documenter.AnchoredHeader, page, doc; kwargs...)
894927
anchor = header.anchor
895-
id = sanitized_anchor_label(anchor)
928+
id = replace(sanitized_anchor_label(anchor), " " => "-") # potentially use MarkdownAST.mdflatten here?
896929
heading = first(node.children)
897930
println(io)
898931
print(io, "#"^(heading.element.level), " ")
899932
heading_iob = IOBuffer()
900933
render(heading_iob, mime, node, heading.children, page, doc; kwargs...)
901934
heading_text = rstrip(String(take!(heading_iob)))
902935
print(io, heading_text)
903-
if id != heading_text # if a custom ID is set, then use it.
904-
print(io, " {#$(replace(id, " " => "-"))}") # potentially use MarkdownAST.mdflatten here?
936+
print(io, " {#$(id)}")
937+
if haskey(kwargs, :inventory)
938+
item = InventoryItem(
939+
name = header.anchor.id,
940+
domain = "std",
941+
role = "label",
942+
dispname = _get_inventory_dispname(header.anchor.id, Documenter.MDFlatten.mdflatten(anchor.node)),
943+
priority = -1,
944+
uri = _get_inventory_uri(doc, page, id),
945+
)
946+
push!(kwargs[:inventory], item)
905947
end
906948
println(io)
907949
end
@@ -1094,3 +1136,83 @@ function render(io::IO, mime::MIME"text/plain", node::Documenter.MarkdownAST.Nod
10941136
println(io)
10951137
println(io, "![]($image_path)")
10961138
end
1139+
1140+
1141+
function _get_inventory_version(project_toml)
1142+
version = ""
1143+
if isfile(project_toml)
1144+
project_data = TOML.parsefile(project_toml)
1145+
if haskey(project_data, "version")
1146+
version = project_data["version"]
1147+
@info "Automatic `version=$(repr(version))` for inventory from $(relpath(project_toml))"
1148+
else
1149+
@warn "Cannot extract version for inventory from $(project_toml): no version information"
1150+
end
1151+
else
1152+
@warn "Cannot extract version for inventory from $(project_toml): no such file"
1153+
end
1154+
if isempty(version)
1155+
# The automatic `inventory_version` determined in this function is intended only for
1156+
# projects with a standard layout with Project.toml file in the expected
1157+
# location (the parent folder of doc.user.root). Any non-standard project should
1158+
# explicitly set an `inventory_version` as an option to `MarkdownVitepress()` in `makedocs`, or
1159+
# specifically set `inventory_version=""` so that `_get_inventory_version` is never
1160+
# called, and thus this warning is suppressed.
1161+
@warn "Please set `inventory_version` in the `MarkdownVitepress()` options passed to `makedocs`."
1162+
end
1163+
return version
1164+
end
1165+
1166+
1167+
function _get_inventory_uri(doc, page, anchor_id)
1168+
filename = relpath(page.build, doc.user.build)
1169+
page_url = splitext(filename)[1]
1170+
if Sys.iswindows()
1171+
page_url = replace(page_url, "\\" => "/")
1172+
end
1173+
uri = join(map(_escapeuri, split(page_url, "/")), "/")
1174+
if !isnothing(anchor_id)
1175+
uri = uri * "#" * _escapeuri(anchor_id)
1176+
end
1177+
return uri
1178+
end
1179+
1180+
1181+
function _get_inventory_dispname(name, dispname)
1182+
if dispname == name
1183+
dispname = "-"
1184+
end
1185+
return dispname
1186+
end
1187+
1188+
1189+
function _pagetitle(page)
1190+
page_mdast = page.mdast
1191+
@assert page_mdast.element isa MarkdownAST.Document
1192+
title_node = nothing
1193+
for node in page_mdast.children
1194+
# AnchoredHeaders should have just one child node, which is the Heading node
1195+
if isa(node.element, Documenter.AnchoredHeader)
1196+
node = first(node.children)
1197+
end
1198+
if isa(node.element, MarkdownAST.Heading) && node.element.level == 1
1199+
title_node = collect(node.children)
1200+
break
1201+
end
1202+
end
1203+
if isnothing(title_node)
1204+
return "-"
1205+
else
1206+
return Documenter.MDFlatten.mdflatten(title_node)
1207+
end
1208+
end
1209+
1210+
1211+
@inline _issafe(c::Char) =
1212+
c == '-' || c == '.' || c == '_' || (isascii(c) && (isletter(c) || isnumeric(c)))
1213+
1214+
_utf8_chars(str::AbstractString) = (Char(c) for c in codeunits(str))
1215+
1216+
_escapeuri(c::Char) = string('%', uppercase(string(Int(c), base = 16, pad = 2)))
1217+
_escapeuri(str::AbstractString) =
1218+
join(_issafe(c) ? c : _escapeuri(c) for c in _utf8_chars(str))

0 commit comments

Comments
 (0)