From f7e2b38301917dc56e49a90f271b47b908aebe7f Mon Sep 17 00:00:00 2001 From: Travis Cross Date: Wed, 4 Mar 2026 03:54:55 +0000 Subject: [PATCH] Resolve grammar rules in link reference definitions Currently, in our Markdown, we support `[text][RULE_NAME]` and `[text][grammar-RULE_NAME]` for linking to grammar rules, but we don't support this syntax within link reference definitions, i.e., `[text]: grammar-RULE_NAME`, even though we do support linking to (non-grammar) rule identifiers within link reference definitions. That's an inconsistency that continually surprises us. Let's fix that. In this commit, we add `grammar_link_references`, which scans link reference definitions for destinations that match a grammar rule name -- either with a `grammar-` prefix or not. When a match is found, the destination is replaced with the resolved path and anchor, just as `rule_link_references` does for rules. Unrecognized destinations pass through unchanged, falling through to `std_links` for rustdoc resolution -- the same behavior as unresolved `[text][NAME]` reference links. We also update the dev-guide to document the new feature in both `links.md` and `grammar.md`. --- dev-guide/src/grammar.md | 14 +++++++++++ dev-guide/src/links.md | 4 ++++ tools/mdbook-spec/src/grammar.rs | 41 ++++++++++++++++++++++++++++++++ tools/mdbook-spec/src/lib.rs | 1 + 4 files changed, 60 insertions(+) diff --git a/dev-guide/src/grammar.md b/dev-guide/src/grammar.md index 2d9b22756d..af1c23ba6e 100644 --- a/dev-guide/src/grammar.md +++ b/dev-guide/src/grammar.md @@ -154,5 +154,19 @@ The [`mdbook-spec`] plugin automatically adds Markdown link definitions for all In some cases, there might be name collisions with the automatic linking of rule names. In that case, disambiguate with the `grammar-` prefix, such as `[Type][grammar-Type]`. The prefix can also be used when explicitness would aid clarity. +Production names can also be used in link reference definitions to provide custom link text, both with and without the `grammar-` prefix. + +```markdown +We accept any [type]. + +[type]: grammar-Type +``` + +```markdown +We accept any [type]. + +[type]: Type +``` + [`mdbook-spec`]: tooling/mdbook-spec.md [Notation]: https://doc.rust-lang.org/nightly/reference/notation.html diff --git a/dev-guide/src/links.md b/dev-guide/src/links.md index fb2199c0dc..20458758a6 100644 --- a/dev-guide/src/links.md +++ b/dev-guide/src/links.md @@ -74,6 +74,10 @@ Link definitions are automatically generated for all grammar production names. S This attribute uses the [MetaWord] syntax. Explicit grammar links can have the `grammar-` prefix like [Type][grammar-Type]. + +Grammar links can also appear in link reference definitions, e.g. [type]. + +[type]: grammar-Type ``` ## Outside book links diff --git a/tools/mdbook-spec/src/grammar.rs b/tools/mdbook-spec/src/grammar.rs index 12ece5df7a..8aa98d3608 100644 --- a/tools/mdbook-spec/src/grammar.rs +++ b/tools/mdbook-spec/src/grammar.rs @@ -75,6 +75,47 @@ pub fn insert_grammar(grammar: &Grammar, chapter: &Chapter, diag: &mut Diagnosti content } +/// Converts link reference definitions that point to a grammar rule +/// to the correct link. +/// +/// For example: +/// +/// ```markdown +/// We accept any [token]. +/// +/// [token]: grammar-Token +/// ``` +/// +/// This will convert the `[token]` definition to point +/// to the actual link. +/// +/// This supports both a `grammar-` prefixed form (e.g. +/// `grammar-Token`) and a bare rule name (e.g. `Token`). +pub fn grammar_link_references(chapter: &Chapter, grammar: &Grammar) -> String { + let current_path = chapter.path.as_ref().unwrap().parent().unwrap(); + let for_summary = is_summary(chapter); + crate::MD_LINK_REFERENCE_DEFINITION + .replace_all(&chapter.content, |caps: &Captures<'_>| { + let dest = &caps["dest"]; + let name = dest.strip_prefix("grammar-").unwrap_or(dest); + if let Some(production) = grammar.productions.get(name) { + let label = &caps["label"]; + let relative = pathdiff::diff_paths(&production.path, current_path).unwrap(); + // Adjust paths for Windows. + let relative = relative.display().to_string().replace('\\', "/"); + let id = render_markdown::markdown_id(name, for_summary); + if for_summary { + format!("[{label}]: #{id}") + } else { + format!("[{label}]: {relative}#{id}") + } + } else { + caps.get(0).unwrap().as_str().to_string() + } + }) + .to_string() +} + /// Creates a map of production name -> relative link path. fn make_relative_link_map(grammar: &Grammar, chapter: &Chapter) -> HashMap { let current_path = chapter.path.as_ref().unwrap().parent().unwrap(); diff --git a/tools/mdbook-spec/src/lib.rs b/tools/mdbook-spec/src/lib.rs index 918508a6df..b94d296940 100644 --- a/tools/mdbook-spec/src/lib.rs +++ b/tools/mdbook-spec/src/lib.rs @@ -168,6 +168,7 @@ impl Preprocessor for Spec { } ch.content = admonitions::admonitions(&ch, &mut diag); ch.content = self.rule_link_references(&ch, &rules); + ch.content = grammar::grammar_link_references(&ch, &grammar); ch.content = self.auto_link_references(&ch, &rules); ch.content = self.render_rule_definitions(&ch.content, &tests, &git_ref); if ch.name == "Test summary" {