diff --git a/src/cross_references.jl b/src/cross_references.jl index 35389d515d..1dfaa286b8 100644 --- a/src/cross_references.jl +++ b/src/cross_references.jl @@ -228,9 +228,15 @@ function xref_unresolved(node) occursin(XREF_REGEX, node.element.destination) end - function Selectors.matcher(::Type{XRefResolvers.Header}, node, slug, meta, page, doc, errors) - return (xref_unresolved(node) && anchor_exists(doc.internal.headers, slug)) + xref_unresolved(node) || return false + dest = xrefname(node.element.destination) + + if isempty(dest) + return first(linkcontent(node)) ∈ (:text, :complex) + else + return !startswith(dest, "#") && occursin(HEADER_REGEX, dest) + end end function Selectors.runner(::Type{XRefResolvers.Header}, node, slug, meta, page, doc, errors) @@ -248,7 +254,15 @@ end function Selectors.matcher(::Type{XRefResolvers.Docs}, node, slug, meta, page, doc, errors) - return xref_unresolved(node) + xref_unresolved(node) || return false + + dest = xrefname(node.element.destination) + + if isempty(dest) + return first(linkcontent(node)) == :code + else + return !startswith(dest, "#") + end end function Selectors.runner(::Type{XRefResolvers.Docs}, node, slug, meta, page, doc, errors) @@ -285,15 +299,7 @@ function xref(node::MarkdownAST.Node, meta, page, doc) slug = xrefname(link.destination) @assert !isnothing(slug) if isempty(slug) - # obtain a slug from the link text - if length(node.children) == 1 && isa(first(node.children).element, MarkdownAST.Code) - slug = first(node.children).element.code - else - # TODO: remove this hack (replace with mdflatten?) - md = _link_node_as_md(node) - text = strip(sprint(Markdown.plain, Markdown.Paragraph(md.content[1].content[1].text))) - slug = Documenter.slugify(text) - end + slug = Documenter.slugify(last(linkcontent(node))) else # explicit slugs that are enclosed in quotes must be further sluggified stringmatch = match(r"\"(.+)\"", slug) @@ -341,6 +347,22 @@ function xrefname(link_url::AbstractString) return isnothing(m[1]) ? "" : strip(m[1]) end +function linkcontent(node::MarkdownAST.Node) + isa(node.element, MarkdownAST.Link) || return nothing + + if length(node.children) == 1 + child = first(node.children).element + if isa(child, MarkdownAST.Code) + return (:code, child.code) + elseif isa(child, MarkdownAST.Text) + return (:text, child.text) + end + end + + text = MDFlatten.mdflatten(node) + return (:complex, text) +end + """Regular expression for an `@ref` link url. This is used by the [`XRefResolvers.XRefResolverPipeline`](@ref), respectively @@ -350,6 +372,10 @@ pipeline. """ const XREF_REGEX = r"^\s*@ref(\s.*)?$" +"""Regular expression for a slug +""" +const HEADER_REGEX = r"^\".+\"$" + # Cross referencing headers. # -------------------------- @@ -357,21 +383,23 @@ const XREF_REGEX = r"^\s*@ref(\s.*)?$" function namedxref(node::MarkdownAST.Node, slug, meta, page, doc, errors) @assert node.element isa MarkdownAST.Link headers = doc.internal.headers - @assert anchor_exists(headers, slug) - # Add the link to list of local uncheck links. - doc.internal.locallinks[node.element] = node.element.destination - # Error checking: `slug` should exist and be unique. - # TODO: handle non-unique slugs. - if anchor_isunique(headers, slug) - # Replace the `@ref` url with a path to the referenced header. - anchor = Documenter.anchor(headers, slug) - pagekey = relpath(anchor.file, doc.user.build) - page = doc.blueprint.pages[pagekey] - node.element = Documenter.PageLink(page, anchor_label(anchor)) + if anchor_exists(headers, slug) + # Add the link to list of local uncheck links. + doc.internal.locallinks[node.element] = node.element.destination + # Error checking: `slug` should exist and be unique. + # TODO: handle non-unique slugs. + if anchor_isunique(headers, slug) + # Replace the `@ref` url with a path to the referenced header. + anchor = Documenter.anchor(headers, slug) + pagekey = relpath(anchor.file, doc.user.build) + page = doc.blueprint.pages[pagekey] + node.element = Documenter.PageLink(page, anchor_label(anchor)) + else + push!(errors, "Header with slug '$slug' is not unique in $(Documenter.locrepr(page.source)).") + end else - push!(errors, "Header with slug '$slug' is not unique in $(Documenter.locrepr(page.source)).") + push!(errors, "Header with slug '$slug' in $(Documenter.locrepr(page.source)) does not exist.") end - return end # Cross referencing docstrings. diff --git a/test/docsxref/make.jl b/test/docsxref/make.jl index 1d31f6f134..0e2bce8620 100644 --- a/test/docsxref/make.jl +++ b/test/docsxref/make.jl @@ -52,23 +52,66 @@ end captured = IOCapture.capture() do makedocs(; kwargs...) end + + output = replace(captured.output, + "\\src\\index" => "/src/index", + "\\src\\page" => "/src/page") + @test isnothing(captured.value) - @test contains( - replace(captured.output, "\\src\\index" => "/src/index"), + @test contains(output, """ ┌ Warning: Cannot resolve @ref for md"[`AbstractSelector`](@ref)" in docsxref/src/index.md. │ - No docstring found in doc for binding `Main.DocsReferencingMain.AbstractSelector`. │ - Fallback resolution in Main for `AbstractSelector` -> `Documenter.Selectors.AbstractSelector` is only allowed for fully qualified names """ ) - @test contains( - replace(captured.output, "\\src\\page" => "/src/page"), + @test contains(output, """ ┌ Warning: Cannot resolve @ref for md"[`DocsReferencingMain.f`](@ref)" in docsxref/src/page.md. │ - Exception trying to find docref for `DocsReferencingMain.f`: unable to get the binding for `DocsReferencingMain.f` in module Documenter.Selectors │ - Fallback resolution in Main for `DocsReferencingMain.f` -> `Main.DocsReferencingMain.f` is only allowed for fully qualified names """ ) + + @test contains(output, + """ + ┌ Warning: Cannot resolve @ref for md"[header](@ref)" in docsxref/src/index.md. + │ - Header with slug 'header' in docsxref/src/index.md does not exist. + """ + ) + @test contains(output, + """ + ┌ Warning: Cannot resolve @ref for md"[header link](@ref \\\"header\\\")" in docsxref/src/index.md. + │ - Header with slug 'header' in docsxref/src/index.md does not exist. + """ + ) + + @test contains(output, + """ + ┌ Warning: Cannot resolve @ref for md"[Multiple words](@ref)" in docsxref/src/index.md. + │ - Header with slug 'Multiple-words' in docsxref/src/index.md does not exist. + """ + ) + @test contains(output, + """ + ┌ Warning: Cannot resolve @ref for md"[header link](@ref \\\"Multiple words\\\")" in docsxref/src/index.md. + │ - Header with slug 'Multiple-words' in docsxref/src/index.md does not exist. + """ + ) + + @test contains(output, + """ + ┌ Warning: Cannot resolve @ref for md"[`foobar`](@ref)" in docsxref/src/index.md. + │ - No docstring found in doc for binding `Main.foobar`. + """ + ) + @test contains(output, + """ + ┌ Warning: Cannot resolve @ref for md"[docstring link](@ref Main.foobar)" in docsxref/src/index.md. + │ - No docstring found in doc for binding `Main.foobar`. + """ + ) + index_html = joinpath(dirname(@__FILE__), "build", "index.html") @test isfile(index_html) if isfile(index_html) @@ -77,6 +120,15 @@ end @test contains(html, "Documenter.Selectors.AbstractSelector") @test contains(html, "Main.AbstractSelector") @test contains(html, "AbstractSelector") + + @test contains(html, "API") + @test contains(html, "header link") + @test contains(html, "Two words") + @test contains(html, "header link") + @test contains(html, "#12345") + @test contains(html, "issue link") + @test contains(html, "DocsReferencingMain.g") + @test contains(html, "docstring link") end page_html = joinpath(dirname(@__FILE__), "build", "page.html") @test isfile(page_html) @@ -88,6 +140,7 @@ end @test contains(html, "Documenter.hide") end + end end diff --git a/test/docsxref/src/index.md b/test/docsxref/src/index.md index e119cc2279..54154e963d 100644 --- a/test/docsxref/src/index.md +++ b/test/docsxref/src/index.md @@ -2,6 +2,27 @@ On the *page* (unlike in the `g` docstring) can link directly to [`AbstractSelector`](@ref) because the `CurrentModule` is `Main`. +Implicit link to a header (single word): [API](@ref). +Explicit link to a header (single word): [header link](@ref "API"). + +Implicit link to a header (multiple words): [Two words](@ref). +Explicit link to a header (multiple words): [header link](@ref "Two words"). + +Implicit link to a non-existent header (single word): [header](@ref). +Explicit link to a non-existent header (single word): [header link](@ref "header"). + +Implicit link to a non-existent header (multiple words): [Multiple words](@ref). +Explicit link to a non-existent header (multiple words): [header link](@ref "Multiple words"). + +Implicit link to an issue: [#12345](@ref). +Explicit link to an issue: [issue link](@ref #12345). + +Implicit link to a docstring: [`DocsReferencingMain.g`](@ref). +Explicit link to a docstring: [docstring link](@ref DocsReferencingMain.g). + +Implicit link to a non-existent docstring: [`foobar`](@ref). +Explicit link to a non-existent docstring: [docstring link](@ref Main.foobar). + ## API ```@docs @@ -13,3 +34,7 @@ DocsReferencingMain.g Documenter.Selectors.AbstractSelector Documenter.hide ``` + +## Two words + +Something