utkarshkukreti / markup.rs

A blazing fast, type-safe template engine for Rust.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Provide some means to deduplicate the base template

ssokolow opened this issue · comments

I currently have to manually include something like this in every top-level template I write and it'd be really nice if I could deduplicate it.

        @markup::doctype()
        html[lang="en", class=dark_mode.then(|| "dark_mode")] {
            @Header { title: &format!("Error {status_code}"), robots_meta, build_timestamp }

            body {
                #container {
                    // REST OF THE TEMPLATE HERE
                }
                @JsBundle { build_timestamp }
            }
        }

I tries playing around with the where clauses and the Rendertrait and making a pair ofFooandFooInner` templates, where the inner template is effectively what an AJAX response would return, but gave up as the need to manually undo the reference-taking for the arguments caused by the outer template calling the inner one made the inner template very cluttered and ugly.

In a more HTML-styled template language, this'd typically be solved either by "template inheritance", which looks like this in Django/Jinja/Twig-style templates...

base.tmpl:

<!DOCTYPE html>
<html lang="en">
    ...
    <body>
        <div id="container">
            {% block content %}ERROR: No Content{% endblock %}
        </div>
    </body>
</html>

specific.tmpl:

{% extends "base.tmpl" %}

{% block content %}
page body goes here
{% endblock %}

...or by abusing the template language's dynamic scoping and runtime evaluation to render "specific.tmpl" by rendering "base.tmpl" and passing in a string argument which is used as the path to include!, relying on how various template languages default to exposing the the template arguments as global variables to what you include!.

Hi,

This is the pattern I use in my own websites:

markup::define! {
    Layout<Content: markup::Render>(content: Content, dark_mode: bool) {
        @markup::doctype()
        html[lang="en", class=dark_mode.then(|| "dark_mode")] {
            body {
                #container {
                    @content
                }
            }
        }
    }
}

// Use

@Layout {
    content: markup::new! {
        div {}
    },
    dark_mode: true,
}

If there are more than a couple of fields that need to be passed to the Layout, e.g. <title>, I create a struct with all the settings and pass that in.

Does this help?

I hadn't tried using markup::new! and that does make it typecheck.

I can't say whether it works yet since I only just finished transcribing the templates over and haven't yet tried switching the actix-web routes over to calling the markup.rs versions.

EDIT: (Upon discovering that the docs for markup::define! were empty, there was no syntax reference in the current README, and finding 962fe14, I more or less concluded this was one of those "the rustdocs are useless" macro crates and habitually exclusively used that syntax reference in the now-deleted version of the README for documentation.)

EDIT: ...and I needn't have explained that thoroughly. The rustdocs for markup::new are empty too, so the rustdocs wouldn't have helped me understand why it exists anyway.

A strange situation I find myself in when I'll probably recommend Sailfish over markdown.rs, not on performance or security considerations, but because it would reflect terribly on me to have to say "Here. The docs are in a deleted version of the README from an old commit."

Sorry about the state of the docs. I'm thinking of just cleaning up the deleted parts of the README and putting it back.

Also to be clear, markup::new! is not strictly required here. You can also do:

markup::define! {
    Layout<Content: markup::Render>(content: Content, dark_mode: bool) {
        @markup::doctype()
        html[lang="en", class=dark_mode.then(|| "dark_mode")] {
            head {
                title { @title }
            }
            body {
                #container {
                    @content
                }
            }
        }
    }

    Index(message: &'static str) {
        h1 { @message }
    }
}

// Use

@Layout {
    content: Index { message: "hello" },
    dark_mode: true,
}

That looks like what I tried, and wound up throwing away after getting errors like this unless I made my templates horrendously ugly:

error[E0277]: can't compare `&&SortMode` with `SortMode`
   --> src/templates.rs:120:27
    |
120 |             @if sort_mode == SortMode::AtoZ {
    |                           ^^ no implementation for `&&SortMode == SortMode`
    |
    = help: the trait `PartialEq<SortMode>` is not implemented for `&&SortMode`

EDIT: By horrendously ugly, I mean turning pretty much every variable access from a simple @foo to something more like { *foo }.

Alternative is matching, but personally I'd be using a direct iterator here:

markup::define! {
  List<T: markup::Render>(items: impl IntoIterator<Item = T>) {
    @for item in items {
      @item
    }
  }
}

Mostly since IntoIterator is more general here, and is returned with a DoubleEndedIterator + Iterator, among others.

Though I personally put sorting differently, as a list of groups which get sorted among themselves... but that's another talk.

That particular example is part of an ugly rapid-protoype that I recently implemented, and will probably be turning into a match once I finish porting the templates, but other examples included turning ThingInner { foo, bar, baz } into something along the lines of ThingInner { foo: { *foo }, bar: { *bar ), baz: { *baz } } being the least ugly way to make things typecheck.