Skip to content

Tighter rules for referencing#2678

Merged
fingolfin merged 8 commits intoJuliaDocs:masterfrom
barucden:xref
Mar 20, 2026
Merged

Tighter rules for referencing#2678
fingolfin merged 8 commits intoJuliaDocs:masterfrom
barucden:xref

Conversation

@barucden
Copy link
Copy Markdown
Contributor

@barucden barucden commented Apr 14, 2025

This is an attempt to clarify the matching criteria for cross-references. I am not very familiar with Documenter's code base, so I am sure the code could be improved, but I am submitting the PR as it is to see if there's broader support for it.

Essentially, the goal is to respect the following rules for references:

Reference type Implicit link Explicit link
Header [Header name](@ref) [link](@ref "Header name")
Issue [#12345](@ref) [link](@ref #12345)
Docstring [`Module.func`](@ref) [link](@ref Module.func)

It means that linking [Nonexistent header](@ref) will be recognized as a link to a header named "Nonexistent header", and never as link to a docstring for Nonexistent-header (ref. #2677).

If this direction seems promising, I'd like to get feedback on how to improve the code, especially linkcontent.


Resolves #2677
Resolves JuliaLang/julia#58073

@goerz
Copy link
Copy Markdown
Member

goerz commented Apr 14, 2025

I think that accurately describes the current situation.

It's a little bit unfortunate that the explicit link for a DocString is [link](@ref Module.func), not [link](@ref `Module.func`). That's always going to be an ambiguity, since the way this is implemented is for Documenter to look up targets in internal indices, and the fall through to the next resolver when that doesn't work. We can't get rid of the ambiguous syntax for backwards-compatibility reasons, but maybe at some point we can also add support for the non-ambiguous [link](@ref `Module.func`) and slowly push people to use that instead. That's what I did in DocumenterInterLinks.

@barucden
Copy link
Copy Markdown
Contributor Author

barucden commented Apr 14, 2025

It's a little bit unfortunate that the explicit link for a DocString is [link](@ref Module.func), not [link](@ref `Module.func`).

Yeah, I thought that too. However, I don't think it's ambiguous if we require that explicit header references are inside double quotes (it is supported at least:

# explicit slugs that are enclosed in quotes must be further sluggified
). That is for implicit references:

  • reference is inside backticks => code
  • reference starts with a # => issue
  • otherwise => header

And for explicit:

  • reference is inside double quotes => header
  • reference starts with a # => issue
  • otherwise => code

The conditions are mutually exclusive this way.

@barucden
Copy link
Copy Markdown
Contributor Author

I made a little progress, but I realized that the rules are indeed ambiguous due to referencing headers by their id:

[link](@ref someid)

[# Header](@id someid)

```@docs
someid
```

@mortenpi
Copy link
Copy Markdown
Member

If there is an @id reference that is the same as a docstring, it should take precedence: https://documenter.juliadocs.org/dev/man/syntax/#Label-precedence-403457a5

Otherwise, the rules in #2678 (comment) seem correct to me! As far as I can tell, this matches what we have documented, even if the implementation doesn't doesn't match. I'm actually surprised that the current implementation seems to be more.. uhm.. loose.. than I though it would be.

What would be nice is to add some tests here (if feasible.. it can be tricky sometimes for this kind of stuff), and also clarify the docs (e.g. the table from the OP would be great to add to the docs probably).

What might be good is to build the Julia manual with this PR before merging, just to see if any warnings crop up.

@barucden
Copy link
Copy Markdown
Contributor Author

Thank you for your input, Morten.

If there is an @id reference that is the same as a docstring, it should take precedence: https://documenter.juliadocs.org/dev/man/syntax/#Label-precedence-403457a5

I see. The problem is though when the user makes a mistake in the id:

See [link](@ref header-wrong)
# [Header](@id header-correct)

In this case, header-wrong is not recognized as a header id and thus is considered as code, in which case it searches for the docs of Base.:(-).


Implicit references are unambiguous

Type Source Reference
Header # Header name [Header name](@ref)
Issue (none) [#12345](@ref)
Code ```@docs Module.function ``` [`Module.function`](@ref)

That means

if startswith(text, "#")
    # process as an issue reference
elseif occursin(r"^`.+`$", text)
    # process as a code reference
else
    # process as a header reference
end

For explicit references, we can allow and prefer backticks when referring to docstrings, as Michael pointed out. Then, the explicit references are unambiguous too:

Type Source Reference
Header # Header name [link](@ref "Header name")
Header (via id) # [Header name](@id header-id) [link](@ref header-id)
Issue (none) [link](@ref #12345)
Code ```@docs Module.function ``` [link](@ref `Module.function`)

leading to the decision

if occursin(r"^\".+\"$", link)
    # process as a header reference
elseif startswith(link, "#")
    # process as an issue reference
elseif occursin(r"^`.+`$", link)
    # process as a code reference
else
    # process as a header reference via id
end

The problem is backwards compatibility of code references. Right now, [link](@ref something) can mean both

  • reference to a header with id something
  • reference to a docstring of something

in this order. So, we can extend the decision algorithm

if occursin(r"^\".+\"$", link)
    # process as a header reference
elseif startswith(link, "#")
    # process as an issue reference
elseif occursin(r"^`.+`$", link)
    # process as a code reference
else
    # process as a header reference via id if there is such a header
    # and otherwise, process as a code reference
end

However, there's that problem with header-wrong leading to the reference to Base.:(-). It really only matters in julia's docs, but those docs are Documenter's prominent user, so I think it warrants adding a little special case:

if occursin(r"^\".+\"$", link)
    # process as a header reference
elseif startswith(link, "#")
    # process as an issue reference
elseif occursin(r"^`.+`$", link)
    # process as a code reference
else
    if link consists of multiple dash-delimited words
        # process as a header reference via id
    else
        # process as a header reference via id if there is such a header
        # and otherwise, process as a code reference
    end
end

I don't think that many people try to use the explicit reference [link](@ref var1-var2] to refer to Base.:(-), so it shouldn't break any documentation. But if it does break any, it is easily fixable by adding backticks [link](@ref `var1-var2`]. On the other hand, all the accidentally wrong references [link](@ref header-wrong] in julia's docs will lead to a warning.

After some time, when breaking change is permitted, we can just drop this complicated logic in favor of the simple one above.

How does that sound?

@fingolfin
Copy link
Copy Markdown
Collaborator

Just discovered this PR.

@barucden I think your plan sounds good!

And I am not worried about someone trying to use [link](@ref var1-var2]. If despite all expectations someone actually does that, I won't feel bad to tell to switch to [link](@ref Base.:(-)] or something like that.

So if you are still willing to work on this, that'd be awesome! I see that in the meantime, a merge conflict has crept in -- hopefully it is not too bad.

@barucden
Copy link
Copy Markdown
Contributor Author

I completely forgot about this PR, thank you for reminding me, Max.

I haven't finished the work even locally, and I am currently busy. If you or anyone else wants to push this forward, please feel free.

barucden and others added 2 commits March 20, 2026 01:01
Classify @ref targets by syntax before dispatch.

Keep header labels ahead of docstrings and support backticked docs.

Add regression coverage, manual syntax docs, and a changelog entry.

Co-authored-by: Codex <codex@openai.com>
fingolfin and others added 2 commits March 20, 2026 10:50
Normalize docsxref output with sequential replace calls.

This avoids the Julia 1.6 method error from the multi-pair String replace path.

Co-authored-by: Codex <codex@openai.com>
@fingolfin
Copy link
Copy Markdown
Collaborator

I've rebased it (resolving the merge conflict) and then asked Codex to complete the work, based on the nice plan sketched by @barucden above. It also tweaked the documentation to explicitly explain the new behavior. It introduced a new helper classifyxref which seems smart to me.

Overall I am happy with the code it produced; it also added tests.

But I'll wait with merging this to give @barucden a chance to react. Also perhaps @goerz or @mortenpi would like to have a look?

While not necessary, I feel people may look at the Documenter
sources to figure out what is 'best practice', and so we should do
this to send a signal.
@barucden
Copy link
Copy Markdown
Contributor Author

Oh wow, it looks exactly how I was going to implement it. Thank you for taking over, Max!

Copy link
Copy Markdown
Member

@goerz goerz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love it!

(except for the tiny grammar nitpick)

Comment thread docs/src/man/syntax.md Outdated
Co-authored-by: Michael Goerz <mail@michaelgoerz.net>
@fingolfin fingolfin merged commit 16a8cdc into JuliaDocs:master Mar 20, 2026
26 of 27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Using @ref with non-existent multiple-word header names Manual: Incorrect Hyperlink for Package Extensions in Performance Tips

4 participants