Trait tags on docs.rs (#17758)

# Objective

Bevy's ECS provides several core traits such as `Component`,
`SystemParam`, etc that determine where a type can be used. When reading
the docs, this currently requires scrolling down to and scanning the
"Trait Implementations" section. Make these core traits more visible.

## Solution

Add a color-coded labels below the type heading denoting the following
types:
- `Component`
  - immutable components are labeled as such
- `Resource`
- `Asset`
- `Event`
- `Plugin` & `PluginGroup`
- `ScheduleLabel` & `SystemSet`
- `SystemParam`

As docs.rs does not provide an option for post-processing the html,
these are added via JS with traits implementations being detected by
scanning the DOM. Rustdoc's html output is unstable, which could
potentially lead to this detection (or the adding of the labels) to
break, however it only needs to work when a new release is deployed and
falls back to the status quo of not displaying these labels.

Idea by JMS55, implementation by Jondolf (see
https://github.com/Jondolf/bevy_docs_extension_demo/).

## Testing

Run this in Bevy's root folder:
```bash
 RUSTDOCFLAGS="--html-after-content docs-rs/trait-tags.html --cfg docsrs_dep" RUSTFLAGS="--cfg docsrs_dep" cargo doc --no-deps -p <some_bevy_package>
```

---

## Showcase
Check it out on
[docs.rs](https://docs.rs/bevy_docs_extension_demo/0.1.1/bevy_docs_extension_demo/struct.TestAllTraits.html)

![trait
tags](https://github.com/user-attachments/assets/a30d8324-41fd-432a-8e49-6d475f143725)

## Release Notes

On docs.rs, Bevy now displays labels indicating which core traits a type
implements:
![trait tags
small](https://github.com/user-attachments/assets/c69b565f-e4bc-4277-9f6b-40830031077d)

If you want to add these to your own crate, check out [these
instructions](https://github.com/bevyengine/bevy/blob/main/docs-rs#3rd-party-crates).

---------

Co-authored-by: Joona Aalto <jondolf.dev@gmail.com>
Co-authored-by: Carter Weinberg <weinbergcarter@gmail.com>
This commit is contained in:
SpecificProtagonist 2025-02-11 23:13:38 +01:00 committed by GitHub
parent d6725d3b1b
commit 5b0d898866
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 208 additions and 2 deletions

View File

@ -59,7 +59,7 @@ jobs:
env: env:
# needs to be in sync with [package.metadata.docs.rs] # needs to be in sync with [package.metadata.docs.rs]
RUSTFLAGS: --cfg docsrs_dep RUSTFLAGS: --cfg docsrs_dep
RUSTDOCFLAGS: -Zunstable-options --cfg=docsrs --generate-link-to-definition RUSTDOCFLAGS: -Zunstable-options --cfg=docsrs --generate-link-to-definition --html-after-content docs-rs/trait-tags.html
run: | run: |
cargo doc \ cargo doc \
-Zunstable-options \ -Zunstable-options \

View File

@ -4102,7 +4102,15 @@ panic = "abort"
# for details on why this is needed. Since dependencies don't expect to be built # for details on why this is needed. Since dependencies don't expect to be built
# with `--cfg docsrs` (and thus fail to compile) we use a different cfg. # with `--cfg docsrs` (and thus fail to compile) we use a different cfg.
rustc-args = ["--cfg", "docsrs_dep"] rustc-args = ["--cfg", "docsrs_dep"]
rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] rustdoc-args = [
"-Zunstable-options",
"--generate-link-to-definition",
# Embed tags to the top of documentation pages for common Bevy traits
# that are implemented by the current type, like `Component` or `Resource`.
# This makes it easier to see at a glance what types are used for.
"--html-after-content",
"docs-rs/trait-tags.html",
]
all-features = true all-features = true
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"] cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]

33
docs-rs/README.md Normal file
View File

@ -0,0 +1,33 @@
# Docs.rs Extensions
This directory includes some templates and styling to extend and modify [rustdoc]'s output
for Bevy's documentation on [docs.rs]. Currently this consists of tags indicating core
`bevy_ecs` traits.
## 3rd Party Crates
To use in your own crate, first copy this folder into your project,
then add the following to your Cargo.toml:
```toml
[package.metadata.docs.rs]
rustc-args = ["--cfg", "docsrs_dep"]
rustdoc-args = [
"--cfg", "docsrs_dep",
"--html-after-content", "docs-rs/trait-tags.html",
]
[lints.rust]
unexpected_cfgs = { check-cfg = ['cfg(docsrs_dep)'] }
```
## Local Testing
Build the documentation with the extension enabled like this:
```bash
RUSTDOCFLAGS="--html-after-content docs-rs/trait-tags.html --cfg docsrs_dep" RUSTFLAGS="--cfg docsrs_dep" cargo doc --no-deps --package <package_name>
```
[rustdoc]: https://doc.rust-lang.org/rustdoc/what-is-rustdoc.html
[docs.rs]: https://docs.rs

165
docs-rs/trait-tags.html Normal file
View File

@ -0,0 +1,165 @@
<script>
// Adds tags to documentation pages for common Bevy traits like `Component` or `Resource`.
// This makes it easier to see at a glance what types are used for.
//
// This extension is passed to `rustdoc` using the `--html-after-content` flag.
// Traits that we want to show as tags.
// Order determines sort order of items in listings.
const bevyTraits = [
'Plugin',
'PluginGroup',
'Component',
'Resource',
'Asset',
'Event',
'ScheduleLabel',
'SystemSet',
'SystemParam',
];
// Find all traits that are implemented by the current type.
const implementedBevyTraits = findImplementedBevyTraits(document);
// If we found any implemented traits, add them as tags to the top of the page.
if (implementedBevyTraits.size > 0) {
// Create a container for the tags.
const heading = document.body.querySelector(".main-heading h1");
const tagContainer = document.createElement('div');
tagContainer.className = 'bevy-tag-container';
heading.appendChild(tagContainer);
// Check if an implemented trait has a `type Mutability = Immutable` associated type.
// This is used to determine if a `Component` is immutable or not.
// TODO: Ideally we should just check the associated types of the `Component` trait,
// but the docs.rs layout makes it tricky to do so in a robust way.
const associatedTypeHeader = document.querySelectorAll(".trait-impl.associatedtype .code-header");
const isImmutable = [...associatedTypeHeader].some(el => el.innerText.includes('type Mutability = Immutable'));
// Create a tag for each implemented trait.
for (let [tagName, href] of implementedBevyTraits) {
if (tagName == 'Component' & isImmutable) {
tagName = 'Immutable Component';
}
// Create the tag and append it to the container.
tagContainer.appendChild(createBevyTag(tagName, href));
}
}
function findImplementedBevyTraits(doc) {
// Traits that are implemented by the current type.
// The key is the trait name, and the value is the href to the trait's documentation.
const implementedTraits = new Map();
// Find all trait implementation headers.
const allTraitHeaders = doc.body.querySelectorAll(
'#trait-implementations-list .impl .code-header, #blanket-implementations-list .impl .code-header'
);
for (const header of allTraitHeaders) {
// We can extract the trait name by removing any generics and splitting the string by spaces.
// This results in ['impl', 'TraitName', 'for', 'TypeName'].
const traitName = removeGenerics(header.innerText).split(' ')[1].trim();
// Find the link to the trait if the anchor element exists.
// Otherwise, the trait is just in plain text.
const traitLinkEl = [...header.children].find(el => el.getAttribute('href')?.includes(`trait.${traitName}.html`));
const href = traitLinkEl?.getAttribute('href');
implementedTraits.set(traitName, href);
}
const implementedBevyTraits = new Map(
[...implementedTraits].filter(([traitName, _]) => bevyTraits.find((x) => x == traitName))
);
return implementedBevyTraits;
}
// Helper function to remove generics from a string of Rust code.
// For example, 'Vec<T>' would become 'Vec'.
function removeGenerics(str) {
// Remove the innermost generics.
const newStr = str.replace(/<([^<>])*>/g, '');
// If there are still generics, perform the removal again recursively.
if (newStr !== str) {
return removeGenerics(newStr);
}
// No more generics to remove.
return newStr;
}
// Helper function to create a tag element with the given name and href,
// if available.
function createBevyTag(tagName, href) {
const el = document.createElement('a');
const kebabCaseName = tagName.toLowerCase().replace(' ', '-');
if (href) {
el.setAttribute('href', href);
}
el.innerText = tagName;
el.className = `bevy-tag ${kebabCaseName}-tag`;
return el;
}
</script>
<style>
.bevy-tag-container {
padding: 0.5rem 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.bevy-tag {
display: flex;
align-items: center;
width: fit-content;
height: 1.5rem;
padding: 0 0.5rem;
border-radius: 0.75rem;
font-size: 1rem;
font-weight: normal;
color: white;
}
.bevy-tag {
background-color: var(--tag-color);
}
.component-tag,
.immutable-component-tag {
--tag-color: oklch(50% 27% 95);
}
.resource-tag {
--tag-color: oklch(50% 27% 130);
}
.asset-tag {
--tag-color: oklch(50% 27% 0);
}
.event-tag {
--tag-color: oklch(50% 27% 310);
}
.plugin-tag,
.plugingroup-tag {
--tag-color: oklch(50% 27% 50);
}
.schedulelabel-tag,
.systemset-tag {
--tag-color: oklch(50% 27% 270);
}
.systemparam-tag {
--tag-color: oklch(50% 27% 200);
}
</style>