11import Documenter: Documenter, Builder, Expanders, MarkdownAST
22import Documenter. DOM: escapehtml
3+ using DocInventories: DocInventories, Inventory, InventoryItem
4+ using TOML: TOML
35
46import ANSIColoredPrinters
57using 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
7175end
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..
352375end
353376
354377function 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... )
357381end
358382
381405function 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
893926function 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)
907949end
@@ -1094,3 +1136,83 @@ function render(io::IO, mime::MIME"text/plain", node::Documenter.MarkdownAST.Nod
10941136 println (io)
10951137 println (io, " " )
10961138end
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