Skip to content

Commit 3993d73

Browse files
committed
Tighter rules for referencing
1 parent 9bec14d commit 3993d73

File tree

3 files changed

+136
-20
lines changed

3 files changed

+136
-20
lines changed

src/cross_references.jl

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -228,9 +228,16 @@ function xref_unresolved(node)
228228
occursin(XREF_REGEX, node.element.destination)
229229
end
230230

231-
232231
function Selectors.matcher(::Type{XRefResolvers.Header}, node, slug, meta, page, doc, errors)
233-
return (xref_unresolved(node) && anchor_exists(doc.internal.headers, slug))
232+
xref_unresolved(node) || return false
233+
dest = xrefname(node.element.destination)
234+
child = linkcontent(node)
235+
236+
if isempty(dest)
237+
return isa(child, MarkdownAST.Text)
238+
else
239+
return !startswith(dest, "#") && occursin(HEADER_REGEX, dest)
240+
end
234241
end
235242

236243
function Selectors.runner(::Type{XRefResolvers.Header}, node, slug, meta, page, doc, errors)
@@ -248,7 +255,16 @@ end
248255

249256

250257
function Selectors.matcher(::Type{XRefResolvers.Docs}, node, slug, meta, page, doc, errors)
251-
return xref_unresolved(node)
258+
xref_unresolved(node) || return false
259+
260+
dest = xrefname(node.element.destination)
261+
child = linkcontent(node)
262+
263+
if isempty(dest)
264+
return isa(child, MarkdownAST.Code)
265+
else
266+
return !startswith(dest, "#")
267+
end
252268
end
253269

254270
function Selectors.runner(::Type{XRefResolvers.Docs}, node, slug, meta, page, doc, errors)
@@ -341,6 +357,20 @@ function xrefname(link_url::AbstractString)
341357
return isnothing(m[1]) ? "" : strip(m[1])
342358
end
343359

360+
function linkcontent(node::MarkdownAST.Node)
361+
isa(node.element, MarkdownAST.Link) || return nothing
362+
363+
@assert length(node.children) == 1
364+
365+
child = first(node.children).element
366+
367+
if isa(child, MarkdownAST.Code) || isa(child, MarkdownAST.Text)
368+
return child
369+
end
370+
371+
@assert false
372+
end
373+
344374
"""Regular expression for an `@ref` link url.
345375
346376
This is used by the [`XRefResolvers.XRefResolverPipeline`](@ref), respectively
@@ -350,28 +380,34 @@ pipeline.
350380
"""
351381
const XREF_REGEX = r"^\s*@ref(\s.*)?$"
352382

383+
"""Regular expression for a slug
384+
"""
385+
const HEADER_REGEX = r"^\".+\"$"
386+
353387

354388
# Cross referencing headers.
355389
# --------------------------
356390

357391
function namedxref(node::MarkdownAST.Node, slug, meta, page, doc, errors)
358392
@assert node.element isa MarkdownAST.Link
359393
headers = doc.internal.headers
360-
@assert anchor_exists(headers, slug)
361-
# Add the link to list of local uncheck links.
362-
doc.internal.locallinks[node.element] = node.element.destination
363-
# Error checking: `slug` should exist and be unique.
364-
# TODO: handle non-unique slugs.
365-
if anchor_isunique(headers, slug)
366-
# Replace the `@ref` url with a path to the referenced header.
367-
anchor = Documenter.anchor(headers, slug)
368-
pagekey = relpath(anchor.file, doc.user.build)
369-
page = doc.blueprint.pages[pagekey]
370-
node.element = Documenter.PageLink(page, anchor_label(anchor))
394+
if anchor_exists(headers, slug)
395+
# Add the link to list of local uncheck links.
396+
doc.internal.locallinks[node.element] = node.element.destination
397+
# Error checking: `slug` should exist and be unique.
398+
# TODO: handle non-unique slugs.
399+
if anchor_isunique(headers, slug)
400+
# Replace the `@ref` url with a path to the referenced header.
401+
anchor = Documenter.anchor(headers, slug)
402+
pagekey = relpath(anchor.file, doc.user.build)
403+
page = doc.blueprint.pages[pagekey]
404+
node.element = Documenter.PageLink(page, anchor_label(anchor))
405+
else
406+
push!(errors, "Header with slug '$slug' is not unique in $(Documenter.locrepr(page.source)).")
407+
end
371408
else
372-
push!(errors, "Header with slug '$slug' is not unique in $(Documenter.locrepr(page.source)).")
409+
push!(errors, "Header with slug '$slug' in $(Documenter.locrepr(page.source)) does not exist.")
373410
end
374-
return
375411
end
376412

377413
# Cross referencing docstrings.

test/docsxref/make.jl

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,23 +52,68 @@ end
5252
captured = IOCapture.capture() do
5353
makedocs(; kwargs...)
5454
end
55+
56+
println(captured)
57+
58+
output = replace(captured.output,
59+
"\\src\\index" => "/src/index",
60+
"\\src\\page" => "/src/page")
61+
5562
@test isnothing(captured.value)
56-
@test contains(
57-
replace(captured.output, "\\src\\index" => "/src/index"),
63+
@test contains(output,
5864
"""
5965
┌ Warning: Cannot resolve @ref for md"[`AbstractSelector`](@ref)" in docsxref/src/index.md.
6066
│ - No docstring found in doc for binding `Main.DocsReferencingMain.AbstractSelector`.
6167
│ - Fallback resolution in Main for `AbstractSelector` -> `Documenter.Selectors.AbstractSelector` is only allowed for fully qualified names
6268
"""
6369
)
64-
@test contains(
65-
replace(captured.output, "\\src\\page" => "/src/page"),
70+
@test contains(output,
6671
"""
6772
┌ Warning: Cannot resolve @ref for md"[`DocsReferencingMain.f`](@ref)" in docsxref/src/page.md.
6873
│ - Exception trying to find docref for `DocsReferencingMain.f`: unable to get the binding for `DocsReferencingMain.f` in module Documenter.Selectors
6974
│ - Fallback resolution in Main for `DocsReferencingMain.f` -> `Main.DocsReferencingMain.f` is only allowed for fully qualified names
7075
"""
7176
)
77+
78+
@test contains(output,
79+
"""
80+
┌ Warning: Cannot resolve @ref for md"[header](@ref)" in docsxref/src/index.md.
81+
│ - Header with slug 'header' in docsxref/src/index.md does not exist.
82+
"""
83+
)
84+
@test contains(output,
85+
"""
86+
┌ Warning: Cannot resolve @ref for md"[header link](@ref \\\"header\\\")" in docsxref/src/index.md.
87+
│ - Header with slug 'header' in docsxref/src/index.md does not exist.
88+
"""
89+
)
90+
91+
@test contains(output,
92+
"""
93+
┌ Warning: Cannot resolve @ref for md"[Multiple words](@ref)" in docsxref/src/index.md.
94+
│ - Header with slug 'Multiple-words' in docsxref/src/index.md does not exist.
95+
"""
96+
)
97+
@test contains(output,
98+
"""
99+
┌ Warning: Cannot resolve @ref for md"[header link](@ref \\\"Multiple words\\\")" in docsxref/src/index.md.
100+
│ - Header with slug 'Multiple-words' in docsxref/src/index.md does not exist.
101+
"""
102+
)
103+
104+
@test contains(output,
105+
"""
106+
┌ Warning: Cannot resolve @ref for md"[`foobar`](@ref)" in docsxref/src/index.md.
107+
│ - No docstring found in doc for binding `Main.foobar`.
108+
"""
109+
)
110+
@test contains(output,
111+
"""
112+
┌ Warning: Cannot resolve @ref for md"[docstring link](@ref Main.foobar)" in docsxref/src/index.md.
113+
│ - No docstring found in doc for binding `Main.foobar`.
114+
"""
115+
)
116+
72117
index_html = joinpath(dirname(@__FILE__), "build", "index.html")
73118
@test isfile(index_html)
74119
if isfile(index_html)
@@ -77,6 +122,15 @@ end
77122
@test contains(html, "<a href=\"index.html#Documenter.Selectors.AbstractSelector\"><code>Documenter.Selectors.AbstractSelector</code></a>")
78123
@test contains(html, "<a href=\"index.html#Documenter.Selectors.AbstractSelector\"><code>Main.AbstractSelector</code></a>")
79124
@test contains(html, "<a href=\"@ref\"><code>AbstractSelector</code></a>")
125+
126+
@test contains(html, "<a href=\"index.html#API\">API</a>")
127+
@test contains(html, "<a href=\"index.html#API\">header link</a>")
128+
@test contains(html, "<a href=\"index.html#Two-words\">Two words</a>")
129+
@test contains(html, "<a href=\"index.html#Two-words\">header link</a>")
130+
@test contains(html, "<a href=\"https://github.com/JuliaDocs/Documenter.jl/issues/12345\">#12345</a>")
131+
@test contains(html, "<a href=\"https://github.com/JuliaDocs/Documenter.jl/issues/12345\">issue link</a>")
132+
@test contains(html, "<a href=\"index.html#Main.DocsReferencingMain.g\"><code>DocsReferencingMain.g</code></a>")
133+
@test contains(html, "<a href=\"index.html#Main.DocsReferencingMain.g\">docstring link</a>")
80134
end
81135
page_html = joinpath(dirname(@__FILE__), "build", "page.html")
82136
@test isfile(page_html)
@@ -88,6 +142,7 @@ end
88142
@test contains(html, "<a href=\"index.html#Documenter.hide\"><code>Documenter.hide</code></a>")
89143
end
90144

145+
91146
end
92147

93148
end

test/docsxref/src/index.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,27 @@
22

33
On the *page* (unlike in the `g` docstring) can link directly to [`AbstractSelector`](@ref) because the `CurrentModule` is `Main`.
44

5+
Implicit link to a header (single word): [API](@ref).
6+
Explicit link to a header (single word): [header link](@ref "API").
7+
8+
Implicit link to a header (multiple words): [Two words](@ref).
9+
Explicit link to a header (multiple words): [header link](@ref "Two words").
10+
11+
Implicit link to a non-existent header (single word): [header](@ref).
12+
Explicit link to a non-existent header (single word): [header link](@ref "header").
13+
14+
Implicit link to a non-existent header (multiple words): [Multiple words](@ref).
15+
Explicit link to a non-existent header (multiple words): [header link](@ref "Multiple words").
16+
17+
Implicit link to an issue: [#12345](@ref).
18+
Explicit link to an issue: [issue link](@ref #12345).
19+
20+
Implicit link to a docstring: [`DocsReferencingMain.g`](@ref).
21+
Explicit link to a docstring: [docstring link](@ref DocsReferencingMain.g).
22+
23+
Implicit link to a non-existent docstring: [`foobar`](@ref).
24+
Explicit link to a non-existent docstring: [docstring link](@ref Main.foobar).
25+
526
## API
627

728
```@docs
@@ -13,3 +34,7 @@ DocsReferencingMain.g
1334
Documenter.Selectors.AbstractSelector
1435
Documenter.hide
1536
```
37+
38+
## Two words
39+
40+
Something

0 commit comments

Comments
 (0)