diff --git a/docs/make.jl b/docs/make.jl index afd8a82e..0d0f034d 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -4,6 +4,45 @@ using DocumenterCitations using DocumenterInterLinks using LaTeXStrings +struct DecomposeInSidebar + path::String + pages::Any +end + +# So you can only really pull this trick once in a Julia session +# But because doc.user.pages is a Vector{Any}, we can't really do anything about it. +function DocumenterVitepress.pagelist2str(doc, ds::Vector{<: Any}, ::Val{:sidebar}) + println("Hello World!!!") + if !all(x -> x isa DecomposeInSidebar, ds) + # if this is false, invoke the default method. + return invoke(pagelist2str, Tuple{Any, Any, Val{:sidebar}}, doc, ds, Val(:sidebar)) + end + contents = DocumenterVitepress.pagelist2str.((doc,), ds, (Val(:sidebar),)) + ret = "{\n" * join(contents, ",\n") * "\n}" + println(ret) + return ret +end + +function DocumenterVitepress.pagelist2str(doc, ds::DecomposeInSidebar, ::Val{:sidebar}) + raw_contents = DocumenterVitepress.pagelist2str(doc, ds.pages, Val(:sidebar)) + contents = if raw_contents isa String + raw_contents + else + join(raw_contents, ",\n") + end + + return "\"/$(ds.path)/\": {\n$(contents)\n}" +end + +function DocumenterVitepress.pagelist2str(doc, ds::DecomposeInSidebar, ::Val{:navbar}) + return DocumenterVitepress.pagelist2str(doc, ds.pages, Val(:sidebar)) +end + +function Documenter.walk_navpages(ds::DecomposeInSidebar, parent, doc) + return Documenter.walk_navpages(ds.pages, parent, doc) +end + + # Handle DocumenterCitations integration - if you're running this, then you don't need anything here!! documenter_citations_dir = dirname(dirname(pathof(DocumenterCitations))) documenter_citations_docs_dir = joinpath(documenter_citations_dir, "docs") @@ -53,7 +92,7 @@ makedocs(; source = "src", build = "build", pages = [ - "Manual" => [ + DecomposeInSidebar("manual", "Manual" => [ "Get Started" => "manual/get_started.md", "Updating to DocumenterVitepress" => "manual/documenter_to_vitepress_docs_example.md", "Code" => "manual/code_example.md", @@ -63,12 +102,11 @@ makedocs(; "CSS Styling" => "manual/style_css.md", "Authors' badge" => "manual/author_badge.md", "GitHub Icon with Stars" => "manual/repo_stars.md", - ], - "Developers' documentation" => [ + ]), + DecomposeInSidebar("devs", "Developers' documentation" => [ "The rendering process" => "devs/render_pipeline.md", "Internal API" => "devs/internal_api.md", - ], - "api.md", + ]), ], plugins = [bib, links], ) diff --git a/src/DocumenterVitepress.jl b/src/DocumenterVitepress.jl index 00229d1f..824ccaa2 100644 --- a/src/DocumenterVitepress.jl +++ b/src/DocumenterVitepress.jl @@ -15,6 +15,7 @@ const ASSETS = normpath(joinpath(@__DIR__, "..", "assets")) include("vitepress_interface.jl") include("vitepress_config.jl") +include("frontmatter.jl") include("writer.jl") include("ANSIBlocks.jl") diff --git a/src/frontmatter.jl b/src/frontmatter.jl new file mode 100644 index 00000000..f46ecfeb --- /dev/null +++ b/src/frontmatter.jl @@ -0,0 +1,26 @@ +# In Vitepress you can only have one frontmatter block. +# But users / other Documenter stages may inject multiple. +# So, we have a stage that will merge and render all frontmatter blocks +# before doing anything else. + +function merge_and_render_frontmatter(io::IO, mime::MIME"text/yaml", page, doc; kwargs...) + frontmatter = String[] + for block in page.mdast.children + element = block.element + if element isa MarkdownAST.CodeBlock && element.info == "@frontmatter" + push!(frontmatter, element.code) + elseif element isa Documenter.RawNode && startswith(element.text, "---") + push!(frontmatter, join(split(element.text, "\n")[2:end-1], "\n")) + end + end + + if haskey(page.globals.meta, :Title) + pushfirst!(frontmatter, "title: \"$(page.globals.meta[:Title])\"") + end + if haskey(page.globals.meta, :Description) + pushfirst!(frontmatter, "description: \"$(page.globals.meta[:Description])\"") + end + println(io, "---") + println(io, join(frontmatter, "\n")) + println(io, "---") +end diff --git a/src/vitepress_config.jl b/src/vitepress_config.jl index f3388c53..4af763f8 100644 --- a/src/vitepress_config.jl +++ b/src/vitepress_config.jl @@ -89,14 +89,19 @@ function modify_config_file(doc, settings, deploy_decision, i_folder, base) @info "Base is \"\" and ENV[\"CI\"] is not set so this is a local build. Not adding any additional base prefix based on the repository or deploy url and instead using absolute path \"/\" to facilitate serving docs locally." "/" elseif isnothing(settings.deploy_url) - "/" * splitpath(settings.repo)[end] # Get the last identifier of the repo path, i.e., `user/$repo`. + "/" * split(settings.repo, '/')[end] # Get the last identifier of the repo path, i.e., `user/$repo`. else - s_path = startswith(settings.deploy_url, r"http[s?]:\/\/") ? splitpath(settings.deploy_url)[2:end] : splitpath(settings.deploy_url) - s = length(s_path) > 1 ? joinpath(s_path) : "" # ignore custom URL here + s_path = if startswith(settings.deploy_url, r"http[s?]:\/\/") + frags = split(settings.deploy_url, '/') # "https", "", "my.custom.domain", "sub", "dir" + length(frags) >= 4 ? frags[4:end] : [""] # |-> "sub", "dir" + else + split(settings.deploy_url, '/') # "sub", "dir" + end + s = join(s_path, '/') isempty(s) ? "/" : "/$(s)" end - base_str = deploy_abspath == "/" ? "base: '$(deploy_abspath)$(deploy_relpath)'" : "base: '$(deploy_abspath)/$(deploy_relpath)'" + base_str = endswith(deploy_abspath, "/") ? "base: '$(deploy_abspath)$(deploy_relpath)'" : "base: '$(deploy_abspath)/$(deploy_relpath)'" push!(replacers, "REPLACE_ME_DOCUMENTER_VITEPRESS_DEPLOY_ABSPATH" => deploy_abspath) push!(replacers, "base: 'REPLACE_ME_DOCUMENTER_VITEPRESS'" => base_str) @@ -106,10 +111,10 @@ function modify_config_file(doc, settings, deploy_decision, i_folder, base) # # Vitepress navbar and sidebar provided_page_list = doc.user.pages - sidebar_navbar_info = pagelist2str.((doc,), provided_page_list) - sidebar_navbar_string = join(sidebar_navbar_info, ",\n") - push!(replacers, "sidebar: 'REPLACE_ME_DOCUMENTER_VITEPRESS'" => "sidebar: [\n$sidebar_navbar_string\n]\n") - push!(replacers, "nav: 'REPLACE_ME_DOCUMENTER_VITEPRESS'" => "nav: [\n$sidebar_navbar_string\n]\n") + sidebar_info = sprint(print, pagelist2str(doc, provided_page_list, Val(:sidebar))) + navbar_info = sprint(print, pagelist2str(doc, provided_page_list, Val(:navbar))) + push!(replacers, "sidebar: 'REPLACE_ME_DOCUMENTER_VITEPRESS'" => "sidebar: $(sidebar_info)\n") + push!(replacers, "nav: 'REPLACE_ME_DOCUMENTER_VITEPRESS'" => "nav: $(navbar_info)\n") # # Title push!(replacers, "title: 'REPLACE_ME_DOCUMENTER_VITEPRESS'" => "title: '$(doc.user.sitename)'") @@ -158,10 +163,9 @@ function modify_config_file(doc, settings, deploy_decision, i_folder, base) return end -function _get_raw_text(element) -end +# Utility methods to get data about pages -function pagelist2str(doc, page::String) +function get_title(doc, page::AbstractString) # If no name is given, find the first header in the page, # and use that as the name. elements = collect(doc.blueprint.pages[page].mdast.children) @@ -174,35 +178,50 @@ function pagelist2str(doc, page::String) else Documenter.MDFlatten.mdflatten(elements[idx]) end - return "{ text: '$(replace(name, "'" => "\\'"))', link: '/$(splitext(page)[1])' }" # , $(sidebar_items(doc, page)) }" + return name end +get_title(doc, page::Pair{<: AbstractString, <: Any}) = first(page) -pagelist2str(doc, name_any::Pair{String, <: Any}) = pagelist2str(doc, first(name_any) => last(name_any)) +# Catch all method: just broadcast over any iterable assuming it is a collection +function pagelist2str(doc, pages, sidenav::Val) + contents = map(pages) do page + "{ " * pagelist2str(doc, page, sidenav) * " }" + end + return "[" * join(contents, ",\n") * "]" +end +function pagelist2str(doc, page::AbstractString, sidenav::Val) + name = get_title(doc, page) + return "text: '$(replace(name, "'" => "\\'"))', link: '/$(splitext(page)[1])'" # , $(sidebar_items(doc, page)) }" +end -function pagelist2str(doc, name_page::Pair{String, String}) +function pagelist2str(doc, name_page::Pair{<: Any, <: Any}, sidenav::Val) name, page = name_page # This is the simplest and easiest case. - return "{ text: '$(replace(name, "'" => "\\'"))', link: '/$(splitext(page)[1])' }" # , $(sidebar_items(doc, page)) }" + return pagelist2str(doc, name => page, sidenav) # , $(sidebar_items(doc, page)) }" end -function pagelist2str(doc, name_contents::Pair{String, <: AbstractVector}) - name, contents = name_contents - # This is for nested stuff. Should work automatically but you never know... - rendered_contents = pagelist2str.((doc,), contents) - return "{ text: '$(replace(name, "'" => "\\'"))', collapsed: false, items: [\n$(join(rendered_contents, ",\n"))]\n }" # TODO: add a link here if the name is the same name as a file? +function pagelist2str(doc, name_page::Pair{<: Any, <: Nothing}, sidenav::Val) + return "" end -function sidebar_items(doc, page::String) - # We look at the page elements, and obtain all level 1 and 2 headers. - elements = doc.blueprint.pages[page].elements - headers = filter(x -> x isa Union{MarkdownAST.Heading{1}, MarkdownAST.Heading{2}}, elements) - # If nothing is found, move on in life - if length(headers) ≤ 1 - return "" +function pagelist2str(doc, name_page::Pair{<: AbstractString, <: AbstractString}, sidenav::Val) + name, page = name_page + # This is the simplest and easiest case. + return "text: '$(replace(name, "'" => "\\'"))', link: '/$(splitext(page)[1])'" # , $(sidebar_items(doc, page)) }" +end +function pagelist2str(doc, name_contents::Pair{<: AbstractString, <: AbstractArray}, sidenav::Val) + name, contents = name_contents + # This is for nested stuff. Should work automatically but you never know... + rendered_contents = map(contents) do content + "{" * pagelist2str(doc, content, sidenav) * "}" + end + final_contents = join(rendered_contents, ",\n") + collapse = if sidenav === Val(:sidebar) + "collapsed: false," + else + "" end - # Otherwise, we return a collapsible tree of headers for each level 1 and 2 header. - items = headers - return "collapsed: true, items: [\n $(join(_item_link.((page,), items), ",\n"))\n]" + return "text: '$(replace(name, "'" => "\\'"))', $collapse items: [\n$(final_contents)\n]" # TODO: add a link here if the name is the same name as a file? end function _item_link(page, item) diff --git a/src/writer.jl b/src/writer.jl index d338fb1f..0ed3cf9e 100644 --- a/src/writer.jl +++ b/src/writer.jl @@ -64,6 +64,8 @@ Base.@kwdef struct MarkdownVitepress <: Documenter.Writer assets = nothing "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" inventory_version::Union{String,Nothing} = nothing + "Whether to write inventory or not" + write_inventory = true """ Sets the granularity of versions which should be kept. Options are :patch, :minor or :breaking (the default). You can use this to reduce the number of docs versions that coexist on your dev branch. With :patch, every patch @@ -217,34 +219,47 @@ function render(doc::Documenter.Document, settings::MarkdownVitepress=MarkdownVi end end - version = settings.inventory_version - if isnothing(version) - project_toml = joinpath(dirname(doc.user.root), "Project.toml") - version = _get_inventory_version(project_toml) + inventory = if settings.write_inventory + version = settings.inventory_version + if isnothing(version) + project_toml = joinpath(dirname(doc.user.root), "Project.toml") + version = _get_inventory_version(project_toml) + end + Inventory(; project=doc.user.sitename, version) + else + nothing end - inventory = Inventory(; project=doc.user.sitename, version) # Iterate over the pages, render each page separately for (src, page) in doc.blueprint.pages # This is where you can operate on a per-page level. open(docpath(page.build, builddir, settings.md_output_path), "w") do io + merge_and_render_frontmatter(io, MIME("text/yaml"), page, doc) for node in page.mdast.children - render(io, mime, node, page, doc; inventory) + kwargs = if settings.write_inventory + (; inventory = inventory) + else + (;) + end + render(io, mime, node, page, doc; kwargs...) end end - item = InventoryItem( - name = replace(splitext(src)[1], "\\" => "/"), - domain = "std", - role = "doc", - dispname = _pagetitle(page), - priority = -1, - uri = _get_inventory_uri(doc, page, nothing) - ) - push!(inventory, item) + if settings.write_inventory + item = InventoryItem( + name = replace(splitext(src)[1], "\\" => "/"), + domain = "std", + role = "doc", + dispname = _pagetitle(page), + priority = -1, + uri = _get_inventory_uri(doc, page, nothing) + ) + push!(inventory, item) + end + end + if settings.write_inventory + objects_inv = joinpath(builddir, settings.md_output_path, "public", "objects.inv") + DocInventories.save(objects_inv, inventory) end - - objects_inv = joinpath(builddir, settings.md_output_path, "public", "objects.inv") - DocInventories.save(objects_inv, inventory) bases = determine_bases(deploy_decision.subfolder; settings.keep) @@ -948,6 +963,9 @@ render(io::IO, mime::MIME"text/plain", node::MarkdownAST.Node, ::Documenter.Setu # Raw nodes are used to insert raw HTML into the output. We just print it as is. # TODO: what if the `raw` is not HTML? That is not addressed here but we ought to address it... function render(io::IO, ::MIME"text/plain", node::Documenter.MarkdownAST.Node, raw::Documenter.RawNode, page, doc; kwargs...) + if startswith(raw.text, "---") + return # this was already handled by frontmatter. + end return raw.name === :html ? println(io, raw.text, "\n") : nothing end