Skip to content

Commit 5b0d898

Browse files
SpecificProtagonistJondolfCarter0
authored
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 <[email protected]> Co-authored-by: Carter Weinberg <[email protected]>
1 parent d6725d3 commit 5b0d898

File tree

4 files changed

+208
-2
lines changed

4 files changed

+208
-2
lines changed

.github/workflows/docs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ jobs:
5959
env:
6060
# needs to be in sync with [package.metadata.docs.rs]
6161
RUSTFLAGS: --cfg docsrs_dep
62-
RUSTDOCFLAGS: -Zunstable-options --cfg=docsrs --generate-link-to-definition
62+
RUSTDOCFLAGS: -Zunstable-options --cfg=docsrs --generate-link-to-definition --html-after-content docs-rs/trait-tags.html
6363
run: |
6464
cargo doc \
6565
-Zunstable-options \

Cargo.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4102,7 +4102,15 @@ panic = "abort"
41024102
# for details on why this is needed. Since dependencies don't expect to be built
41034103
# with `--cfg docsrs` (and thus fail to compile) we use a different cfg.
41044104
rustc-args = ["--cfg", "docsrs_dep"]
4105-
rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"]
4105+
rustdoc-args = [
4106+
"-Zunstable-options",
4107+
"--generate-link-to-definition",
4108+
# Embed tags to the top of documentation pages for common Bevy traits
4109+
# that are implemented by the current type, like `Component` or `Resource`.
4110+
# This makes it easier to see at a glance what types are used for.
4111+
"--html-after-content",
4112+
"docs-rs/trait-tags.html",
4113+
]
41064114
all-features = true
41074115
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
41084116

docs-rs/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Docs.rs Extensions
2+
3+
This directory includes some templates and styling to extend and modify [rustdoc]'s output
4+
for Bevy's documentation on [docs.rs]. Currently this consists of tags indicating core
5+
`bevy_ecs` traits.
6+
7+
## 3rd Party Crates
8+
9+
To use in your own crate, first copy this folder into your project,
10+
then add the following to your Cargo.toml:
11+
12+
```toml
13+
[package.metadata.docs.rs]
14+
rustc-args = ["--cfg", "docsrs_dep"]
15+
rustdoc-args = [
16+
"--cfg", "docsrs_dep",
17+
"--html-after-content", "docs-rs/trait-tags.html",
18+
]
19+
20+
[lints.rust]
21+
unexpected_cfgs = { check-cfg = ['cfg(docsrs_dep)'] }
22+
```
23+
24+
## Local Testing
25+
26+
Build the documentation with the extension enabled like this:
27+
28+
```bash
29+
RUSTDOCFLAGS="--html-after-content docs-rs/trait-tags.html --cfg docsrs_dep" RUSTFLAGS="--cfg docsrs_dep" cargo doc --no-deps --package <package_name>
30+
```
31+
32+
[rustdoc]: https://doc.rust-lang.org/rustdoc/what-is-rustdoc.html
33+
[docs.rs]: https://docs.rs

docs-rs/trait-tags.html

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
<script>
2+
// Adds tags to documentation pages for common Bevy traits like `Component` or `Resource`.
3+
// This makes it easier to see at a glance what types are used for.
4+
//
5+
// This extension is passed to `rustdoc` using the `--html-after-content` flag.
6+
7+
// Traits that we want to show as tags.
8+
// Order determines sort order of items in listings.
9+
const bevyTraits = [
10+
'Plugin',
11+
'PluginGroup',
12+
'Component',
13+
'Resource',
14+
'Asset',
15+
'Event',
16+
'ScheduleLabel',
17+
'SystemSet',
18+
'SystemParam',
19+
];
20+
21+
// Find all traits that are implemented by the current type.
22+
const implementedBevyTraits = findImplementedBevyTraits(document);
23+
24+
// If we found any implemented traits, add them as tags to the top of the page.
25+
if (implementedBevyTraits.size > 0) {
26+
// Create a container for the tags.
27+
const heading = document.body.querySelector(".main-heading h1");
28+
const tagContainer = document.createElement('div');
29+
tagContainer.className = 'bevy-tag-container';
30+
heading.appendChild(tagContainer);
31+
32+
// Check if an implemented trait has a `type Mutability = Immutable` associated type.
33+
// This is used to determine if a `Component` is immutable or not.
34+
// TODO: Ideally we should just check the associated types of the `Component` trait,
35+
// but the docs.rs layout makes it tricky to do so in a robust way.
36+
const associatedTypeHeader = document.querySelectorAll(".trait-impl.associatedtype .code-header");
37+
const isImmutable = [...associatedTypeHeader].some(el => el.innerText.includes('type Mutability = Immutable'));
38+
39+
// Create a tag for each implemented trait.
40+
for (let [tagName, href] of implementedBevyTraits) {
41+
if (tagName == 'Component' & isImmutable) {
42+
tagName = 'Immutable Component';
43+
}
44+
45+
// Create the tag and append it to the container.
46+
tagContainer.appendChild(createBevyTag(tagName, href));
47+
}
48+
}
49+
50+
function findImplementedBevyTraits(doc) {
51+
// Traits that are implemented by the current type.
52+
// The key is the trait name, and the value is the href to the trait's documentation.
53+
const implementedTraits = new Map();
54+
55+
// Find all trait implementation headers.
56+
const allTraitHeaders = doc.body.querySelectorAll(
57+
'#trait-implementations-list .impl .code-header, #blanket-implementations-list .impl .code-header'
58+
);
59+
60+
for (const header of allTraitHeaders) {
61+
// We can extract the trait name by removing any generics and splitting the string by spaces.
62+
// This results in ['impl', 'TraitName', 'for', 'TypeName'].
63+
const traitName = removeGenerics(header.innerText).split(' ')[1].trim();
64+
65+
// Find the link to the trait if the anchor element exists.
66+
// Otherwise, the trait is just in plain text.
67+
const traitLinkEl = [...header.children].find(el => el.getAttribute('href')?.includes(`trait.${traitName}.html`));
68+
const href = traitLinkEl?.getAttribute('href');
69+
70+
implementedTraits.set(traitName, href);
71+
}
72+
73+
const implementedBevyTraits = new Map(
74+
[...implementedTraits].filter(([traitName, _]) => bevyTraits.find((x) => x == traitName))
75+
);
76+
77+
return implementedBevyTraits;
78+
}
79+
80+
// Helper function to remove generics from a string of Rust code.
81+
// For example, 'Vec<T>' would become 'Vec'.
82+
function removeGenerics(str) {
83+
// Remove the innermost generics.
84+
const newStr = str.replace(/<([^<>])*>/g, '');
85+
86+
// If there are still generics, perform the removal again recursively.
87+
if (newStr !== str) {
88+
return removeGenerics(newStr);
89+
}
90+
91+
// No more generics to remove.
92+
return newStr;
93+
}
94+
95+
// Helper function to create a tag element with the given name and href,
96+
// if available.
97+
function createBevyTag(tagName, href) {
98+
const el = document.createElement('a');
99+
const kebabCaseName = tagName.toLowerCase().replace(' ', '-');
100+
101+
if (href) {
102+
el.setAttribute('href', href);
103+
}
104+
105+
el.innerText = tagName;
106+
el.className = `bevy-tag ${kebabCaseName}-tag`;
107+
return el;
108+
}
109+
</script>
110+
111+
<style>
112+
.bevy-tag-container {
113+
padding: 0.5rem 0;
114+
display: flex;
115+
flex-wrap: wrap;
116+
gap: 0.5rem;
117+
}
118+
119+
.bevy-tag {
120+
display: flex;
121+
align-items: center;
122+
width: fit-content;
123+
height: 1.5rem;
124+
padding: 0 0.5rem;
125+
border-radius: 0.75rem;
126+
font-size: 1rem;
127+
font-weight: normal;
128+
color: white;
129+
}
130+
131+
.bevy-tag {
132+
background-color: var(--tag-color);
133+
}
134+
135+
.component-tag,
136+
.immutable-component-tag {
137+
--tag-color: oklch(50% 27% 95);
138+
}
139+
140+
.resource-tag {
141+
--tag-color: oklch(50% 27% 130);
142+
}
143+
144+
.asset-tag {
145+
--tag-color: oklch(50% 27% 0);
146+
}
147+
148+
.event-tag {
149+
--tag-color: oklch(50% 27% 310);
150+
}
151+
152+
.plugin-tag,
153+
.plugingroup-tag {
154+
--tag-color: oklch(50% 27% 50);
155+
}
156+
157+
.schedulelabel-tag,
158+
.systemset-tag {
159+
--tag-color: oklch(50% 27% 270);
160+
}
161+
162+
.systemparam-tag {
163+
--tag-color: oklch(50% 27% 200);
164+
}
165+
</style>

0 commit comments

Comments
 (0)