rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
97.33k stars 12.58k forks source link

Tracking Issue for Rustdoc Askama Migration #108868

Open clubby789 opened 1 year ago

clubby789 commented 1 year ago

Tracking Issue for Rustdoc Askama Migration

Zulip Discussion

This issue is to track the progress of migrating rustdoc's HTML rendering to Askama.

Suggested Workflow

If you'd like to work on a component, please leave a comment claiming it so that work doesn't conflict. Work on translating the current series of format!() and string building calls into an HTML template. Where possible, try to keep as much logic as possible on the Rust side. Make sure to refer to STYLE.md for templating style, especially using comment blocks to minimize whitespace.

A specific example of a migration (and some style considerations) is here.

Components to Migrate

Implementation History

@rustbot label +T-rustdoc +A-rustdoc-ui

jsha commented 1 year ago

I'm working on fn assoc_method and fn inner_full_print.

clubby789 commented 1 year ago

I'm working on fn print_src

jsha commented 1 year ago

As part of this, I'd like to get rid of struct Buffer and the f.alternate() method of emitting text instead of HTML.

Rationale: Using Formatter::alternate() to emit text instead of HTML was introduced to support line-wrapping function declarations over 80 characters. To this day, we only use f.alternate() when emitting function / method declarations, and we only care about the number of characters emitted.

Buffer was introduced as an alternative to f.alternate(): Instead of checking f.alternate() repeatedly throughout format.rs, the buffer itself could carry the information about whether we're outputting text or HTML. Removing the repeated checks of f.alternate() is a good goal - they significantly complicate format.rs. Also, Askama does not currently offer a way to access f.alternate() or format with {:#}, AFAICT. However, Buffer itself also doesn't compose well with Askama. Askama templates implement Display; ideally we would want a stack of structures, each potentially containing other structs that implement Display; then we can interpolate them directly into the parent template without excess allocations.

In the original PR adding f.alternate() support for the purpose of line wrapping, generating and then stripping HTML was considered and discarded. But I think it's the right solution here. Because we control the HTML and because we only care about counting the text output, we can get away with a very simplified HTML processor that knows how to strip tags and count entities (e.g. &) as a single character.

This is a summary of a similar comment I made on Zulip.

jsha commented 1 year ago

Update on the above: I missed a few places where we use {:#} and actually emit the plain output:

I still think it makes sense to filter internally-generated HTML with a limited tag stripper, but it looks like we'll need to be able to actually emit the text rather than just count it, which means processing the entities (no big deal).

djc commented 1 year ago

FWIW, feel free to tag me in reviews if there are questions. Excited to see more usage of Askama in rustdoc!

As part of this, I'd like to get rid of struct Buffer and the f.alternate() method of emitting text instead of HTML.

As a potential point of interest, at work we have started using a very small procedural macro that effectively duplicates a template type to allow having two Templates wrapped around the same input type, one for plaintext and one for HTML. So this:


#[email(template = "contact-email")]
pub struct DomainContactEmail<'a> {
    pub domain: &'a str,
    pub sender: &'a ContactSender<'a>,
    pub message: &'a str,
    pub inbox_url: &'a str,
}

expands to this:

impl<'a: 'email, 'email> email::BodyTemplates<'email> for DomainContactEmail<'a> {
    type Text = DomainContactEmailText<'a, 'email>;
    type Html = DomainContactEmailHtml<'a, 'email>;
}
#[derive(askama::Template)]
#[template(path = "contact-email.txt")]
pub struct DomainContactEmailText<'a, 'email>(&'email DomainContactEmail<'a>);

impl<'a: 'email, 'email> From<&'email DomainContactEmail<'a>>
    for DomainContactEmailText<'a, 'email>
{
    fn from(email: &'email DomainContactEmail<'a>) -> Self {
        Self(email)
    }
}
impl<'a: 'email, 'email> std::ops::Deref for DomainContactEmailText<'a, 'email> {
    type Target = &'email DomainContactEmail<'a>;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
#[derive(askama::Template)]
#[template(path = "contact-email.html")]
pub struct DomainContactEmailHtml<'a, 'email>(&'email DomainContactEmail<'a>);

impl<'a: 'email, 'email> std::ops::Deref for DomainContactEmailHtml<'a, 'email> {
    type Target = &'email DomainContactEmail<'a>;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
impl<'a: 'email, 'email> From<&'email DomainContactEmail<'a>>
    for DomainContactEmailHtml<'a, 'email>
{
    fn from(email: &'email DomainContactEmail<'a>) -> Self {
        Self(email)
    }
}

Maybe this is used too much/duplicating the templates would be too much work for Rustdoc, just thought I'd mention it here as a point in the design space.

jsha commented 1 year ago

I'm working on changing primitive_link_fragment, href_relative_parts, and href_with_root_path to use a template to construct the hrefs with fewer allocations.

And thanks for the proc macro advice, and the kind offer of assistance, @djc!

nicklimmm commented 1 year ago

I'm working on item_struct

jsha commented 1 year ago

Awesome, welcome @nicklimmm!

A reminder to double-check the STYLE.md guide, in particular:

Askama templates support quite sophisticated control flow. To keep our templates simple and understandable, we use only a subset: if and for. In particular we avoid assignments in the template logic and Askama macros. This also may make things easier if we switch to a different Jinja-style template system, like Askama, in the future.

(hah, this needs to be updated since it says "if we switch to ... Askama" :-))

But also, more particularly: Askama lets you write a lot of Rust code inside your templates. But generally we don't want to be writing Rust inside of templates - we want Rust on the outside, producing simple struct that can be interpolated into templates in a straightforward way. So for instance if the template struct contains a complex object with lots of methods, like a DefId or a Context, that's undesirable. Instead, when creating the template struct we'd like to extract the specific things we want from those complex objects: Strings, &str's, Cows, Vecs, slices, etc.

TmLev commented 3 months ago

@clubby789 can I please ask you to update the current status of components? Judging by PRs merged, these components have been migrated: static items, trait aliases, proc macros, primitives, foreign types, opaque types.