Skip to content

Commit e25109d

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

File tree

3 files changed

+135
-29
lines changed

3 files changed

+135
-29
lines changed

src/cross_references.jl

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -228,9 +228,15 @@ 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+
235+
if isempty(dest)
236+
return first(linkcontent(node)) (:text, :complex)
237+
else
238+
return !startswith(dest, "#") && occursin(HEADER_REGEX, dest)
239+
end
234240
end
235241

236242
function Selectors.runner(::Type{XRefResolvers.Header}, node, slug, meta, page, doc, errors)
@@ -248,7 +254,15 @@ end
248254

249255

250256
function Selectors.matcher(::Type{XRefResolvers.Docs}, node, slug, meta, page, doc, errors)
251-
return xref_unresolved(node)
257+
xref_unresolved(node) || return false
258+
259+
dest = xrefname(node.element.destination)
260+
261+
if isempty(dest)
262+
return first(linkcontent(node)) == :code
263+
else
264+
return !startswith(dest, "#")
265+
end
252266
end
253267

254268
function Selectors.runner(::Type{XRefResolvers.Docs}, node, slug, meta, page, doc, errors)
@@ -285,15 +299,7 @@ function xref(node::MarkdownAST.Node, meta, page, doc)
285299
slug = xrefname(link.destination)
286300
@assert !isnothing(slug)
287301
if isempty(slug)
288-
# obtain a slug from the link text
289-
if length(node.children) == 1 && isa(first(node.children).element, MarkdownAST.Code)
290-
slug = first(node.children).element.code
291-
else
292-
# TODO: remove this hack (replace with mdflatten?)
293-
md = _link_node_as_md(node)
294-
text = strip(sprint(Markdown.plain, Markdown.Paragraph(md.content[1].content[1].text)))
295-
slug = Documenter.slugify(text)
296-
end
302+
slug = Documenter.slugify(last(linkcontent(node)))
297303
else
298304
# explicit slugs that are enclosed in quotes must be further sluggified
299305
stringmatch = match(r"\"(.+)\"", slug)
@@ -341,6 +347,22 @@ function xrefname(link_url::AbstractString)
341347
return isnothing(m[1]) ? "" : strip(m[1])
342348
end
343349

350+
function linkcontent(node::MarkdownAST.Node)
351+
isa(node.element, MarkdownAST.Link) || return nothing
352+
353+
if length(node.children) == 1
354+
child = first(node.children).element
355+
if isa(child, MarkdownAST.Code)
356+
return (:code, child.code)
357+
elseif isa(child, MarkdownAST.Text)
358+
return (:text, child.text)
359+
end
360+
end
361+
362+
text = MDFlatten.mdflatten(node)
363+
return (:complex, text)
364+
end
365+
344366
"""Regular expression for an `@ref` link url.
345367
346368
This is used by the [`XRefResolvers.XRefResolverPipeline`](@ref), respectively
@@ -350,28 +372,34 @@ pipeline.
350372
"""
351373
const XREF_REGEX = r"^\s*@ref(\s.*)?$"
352374

375+
"""Regular expression for a slug
376+
"""
377+
const HEADER_REGEX = r"^\".+\"$"
378+
353379

354380
# Cross referencing headers.
355381
# --------------------------
356382

357383
function namedxref(node::MarkdownAST.Node, slug, meta, page, doc, errors)
358384
@assert node.element isa MarkdownAST.Link
359385
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))
386+
if anchor_exists(headers, slug)
387+
# Add the link to list of local uncheck links.
388+
doc.internal.locallinks[node.element] = node.element.destination
389+
# Error checking: `slug` should exist and be unique.
390+
# TODO: handle non-unique slugs.
391+
if anchor_isunique(headers, slug)
392+
# Replace the `@ref` url with a path to the referenced header.
393+
anchor = Documenter.anchor(headers, slug)
394+
pagekey = relpath(anchor.file, doc.user.build)
395+
page = doc.blueprint.pages[pagekey]
396+
node.element = Documenter.PageLink(page, anchor_label(anchor))
397+
else
398+
push!(errors, "Header with slug '$slug' is not unique in $(Documenter.locrepr(page.source)).")
399+
end
371400
else
372-
push!(errors, "Header with slug '$slug' is not unique in $(Documenter.locrepr(page.source)).")
401+
push!(errors, "Header with slug '$slug' in $(Documenter.locrepr(page.source)) does not exist.")
373402
end
374-
return
375403
end
376404

377405
# Cross referencing docstrings.

test/docsxref/make.jl

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,23 +52,66 @@ end
5252
captured = IOCapture.capture() do
5353
makedocs(; kwargs...)
5454
end
55+
56+
output = replace(captured.output,
57+
"\\src\\index" => "/src/index",
58+
"\\src\\page" => "/src/page")
59+
5560
@test isnothing(captured.value)
56-
@test contains(
57-
replace(captured.output, "\\src\\index" => "/src/index"),
61+
@test contains(output,
5862
"""
5963
┌ Warning: Cannot resolve @ref for md"[`AbstractSelector`](@ref)" in docsxref/src/index.md.
6064
│ - No docstring found in doc for binding `Main.DocsReferencingMain.AbstractSelector`.
6165
│ - Fallback resolution in Main for `AbstractSelector` -> `Documenter.Selectors.AbstractSelector` is only allowed for fully qualified names
6266
"""
6367
)
64-
@test contains(
65-
replace(captured.output, "\\src\\page" => "/src/page"),
68+
@test contains(output,
6669
"""
6770
┌ Warning: Cannot resolve @ref for md"[`DocsReferencingMain.f`](@ref)" in docsxref/src/page.md.
6871
│ - Exception trying to find docref for `DocsReferencingMain.f`: unable to get the binding for `DocsReferencingMain.f` in module Documenter.Selectors
6972
│ - Fallback resolution in Main for `DocsReferencingMain.f` -> `Main.DocsReferencingMain.f` is only allowed for fully qualified names
7073
"""
7174
)
75+
76+
@test contains(output,
77+
"""
78+
┌ Warning: Cannot resolve @ref for md"[header](@ref)" in docsxref/src/index.md.
79+
│ - Header with slug 'header' in docsxref/src/index.md does not exist.
80+
"""
81+
)
82+
@test contains(output,
83+
"""
84+
┌ Warning: Cannot resolve @ref for md"[header link](@ref \\\"header\\\")" in docsxref/src/index.md.
85+
│ - Header with slug 'header' in docsxref/src/index.md does not exist.
86+
"""
87+
)
88+
89+
@test contains(output,
90+
"""
91+
┌ Warning: Cannot resolve @ref for md"[Multiple words](@ref)" in docsxref/src/index.md.
92+
│ - Header with slug 'Multiple-words' in docsxref/src/index.md does not exist.
93+
"""
94+
)
95+
@test contains(output,
96+
"""
97+
┌ Warning: Cannot resolve @ref for md"[header link](@ref \\\"Multiple words\\\")" in docsxref/src/index.md.
98+
│ - Header with slug 'Multiple-words' in docsxref/src/index.md does not exist.
99+
"""
100+
)
101+
102+
@test contains(output,
103+
"""
104+
┌ Warning: Cannot resolve @ref for md"[`foobar`](@ref)" in docsxref/src/index.md.
105+
│ - No docstring found in doc for binding `Main.foobar`.
106+
"""
107+
)
108+
@test contains(output,
109+
"""
110+
┌ Warning: Cannot resolve @ref for md"[docstring link](@ref Main.foobar)" in docsxref/src/index.md.
111+
│ - No docstring found in doc for binding `Main.foobar`.
112+
"""
113+
)
114+
72115
index_html = joinpath(dirname(@__FILE__), "build", "index.html")
73116
@test isfile(index_html)
74117
if isfile(index_html)
@@ -77,6 +120,15 @@ end
77120
@test contains(html, "<a href=\"index.html#Documenter.Selectors.AbstractSelector\"><code>Documenter.Selectors.AbstractSelector</code></a>")
78121
@test contains(html, "<a href=\"index.html#Documenter.Selectors.AbstractSelector\"><code>Main.AbstractSelector</code></a>")
79122
@test contains(html, "<a href=\"@ref\"><code>AbstractSelector</code></a>")
123+
124+
@test contains(html, "<a href=\"index.html#API\">API</a>")
125+
@test contains(html, "<a href=\"index.html#API\">header link</a>")
126+
@test contains(html, "<a href=\"index.html#Two-words\">Two words</a>")
127+
@test contains(html, "<a href=\"index.html#Two-words\">header link</a>")
128+
@test contains(html, "<a href=\"https://github.com/JuliaDocs/Documenter.jl/issues/12345\">#12345</a>")
129+
@test contains(html, "<a href=\"https://github.com/JuliaDocs/Documenter.jl/issues/12345\">issue link</a>")
130+
@test contains(html, "<a href=\"index.html#Main.DocsReferencingMain.g\"><code>DocsReferencingMain.g</code></a>")
131+
@test contains(html, "<a href=\"index.html#Main.DocsReferencingMain.g\">docstring link</a>")
80132
end
81133
page_html = joinpath(dirname(@__FILE__), "build", "page.html")
82134
@test isfile(page_html)
@@ -88,6 +140,7 @@ end
88140
@test contains(html, "<a href=\"index.html#Documenter.hide\"><code>Documenter.hide</code></a>")
89141
end
90142

143+
91144
end
92145

93146
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)