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)  ## Release Notes On docs.rs, Bevy now displays labels indicating which core traits a type implements:  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:
parent
d6725d3b1b
commit
5b0d898866
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@ -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 \
|
||||||
|
10
Cargo.toml
10
Cargo.toml
@ -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
33
docs-rs/README.md
Normal 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
165
docs-rs/trait-tags.html
Normal 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>
|
Loading…
Reference in New Issue
Block a user