Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow macro to accept body arguments? #51

Open
asimpletune opened this issue Sep 23, 2024 · 29 comments
Open

Allow macro to accept body arguments? #51

asimpletune opened this issue Sep 23, 2024 · 29 comments

Comments

@asimpletune
Copy link

Shortcodes that take body arguments are wonderful. It allows me to wrap markdown text and voila. It would be nice if the macro syntax were extended to work the same as the shortcode syntax, with an implicit body argument.

@Keats
Copy link
Owner

Keats commented Sep 24, 2024

Yes! I want macros to work like shortcodes in Zola so they are also more useful as components. I'm just debating internally if there are other things we want to change for component-ization, like https://jinjax.scaletti.dev/

@Keats
Copy link
Owner

Keats commented Oct 18, 2024

Ok so JinjaX goes all in the HTML way which doesn't really work when you are templating something else.

Things I'm thinking of after reading their docs and that would work well here:

  1. Rename macro -> component
  2. Be able to mark folders as component library and treat all files in there as components: i'm torn on allowing multiple components per files like we can right now.
  3. Components will be loaded automatically in a global namespace, no need to import them anymore
  4. You can take body arguments
  5. Take inspiration from Zola and have both body and body-less versions of components

Usage would look like that:

// This renders a shortcode taking a body
{% alert(kind="warning") %}
The password needs to be more complex
{% end %}

// This renders a shortcode without a body
{{ input(type="password", name="password") }}

The alert component would get the context: {"kind": "warning", "body": "The password needs to be more complex"}

and they would refer to components defined in components/alert.html (or whatever extension you have) and components/input.html, like shortcodes in Zola

Everything is up for discussion, any thoughts?

@asimpletune
Copy link
Author

asimpletune commented Oct 18, 2024

Yeah, I read the jinx docs when you shared them earlier, and I thought it was very html-y, so probably not a 1:1 roadmap for tera.

Thanks for sharing though. My initial thoughts:

  1. Rename macro -> component

I guess what is the difference between a macro and a component? In my mind it's when it's available. A macro is available at compile time and a component is available only at runtime? If you're not planning on changing that aspect then maybe it makes sense to just leave it as macro and that will reserve "components" for something that is more component-y?

I'd like to hear your thoughts though about why the name change to begin with.

  1. Be able to mark folders as component library and treat all files in there as components

I think this is how most people use macros already, in Zola at least, and shortcodes already introduced the convention. It would certainly be simpler for the user.

The one counter argument that comes to mind is that it's common for people to use a shortcode (in Zola) in conjunction with a macro. The shortcode may literally just call the macro, which gives more code reuse in case you also need the same functionality in a template. For this proposal, the downside of having a dedicated components folder would just be that there can't be a "one file" install, in the case of a shortcode that calls a macro.

The next point expands on this a little more.

: i'm torn on allowing multiple components per files like we can right now.

I think having multiple components/macros in a file is useful. For example, I'm working on a project right now that's like a showcase of various shortcodes and macros. I also plan on accepting user submitted ones too. The default for this project is to strive for at least offering a "one file" install, but some complicated make more sense to be broken up into further macros. It helps to keep them in the same file.

These shortcodes/macros are pretty complicated and I reuse them across projects. They provide stuff like HN style comments with prev/next navigation, code windows examples that are organized into a tabbed window, a carousel style image gallery, etc...

They're all implemented in pure html+css so they're very portable between projects, but it would help to keep the ability to organize the macros (or components) in the same file.

  1. Components will be loaded automatically in a global namespace, no need to import them anymore

This makes sense, especially for new users. There just needs to be a way to maintain grep-ability, which for me is the main benefit of the import statement. So even if it is something like {{ macros::foo() }} then that would give me enough to see where a certain macro is used, etc...

  1. You can take body arguments

Yes!

  1. Take inspiration from Zola and have both body and body-less versions of components

Yes! One ask is to consider any possibility of allowing the end tags to optionally be named? When I'm visually parsing a file and I see {% end %}, it just adds extra cycles for me to recall what was the opening statement. It's not a big deal but it exists elsewhere in the tera language. In those cases I found it helpful.

@bemyak
Copy link

bemyak commented Oct 18, 2024

I love this idea! I requested macro bodies in Tera 2 Wishlist, it would be great to have them.

I like "components" more than "macros". However, for me it still creates extra cognitive load when I try to remember the difference between components and shortcodes. Ideally, I would prefer having only "components", which work both in templates and pages. These can be entirely different things under the hood, but it would be nice to have unified user interface and terminology, because functionally they are so close.

It would also be natural then to have strictly one component per file as we have with shortcodes now.

I hope I'm making any sense here.

@Keats
Copy link
Owner

Keats commented Oct 18, 2024

I guess what is the difference between a macro and a component? In my mind it's when it's available. A macro is available at compile time and a component is available only at runtime? If you're not planning on changing that aspect then maybe it makes sense to just leave it as macro and that will reserve "components" for something that is more component-y?

There's no difference really from the template engine pov, it's the same thing, the macro calls are not replaced in the templates (we could but it just makes error reporting harder). I was thinking of using components as it's much clearer to everyone doing frontend these days than macros.

These shortcodes/macros are pretty complicated and I reuse them across projects. They provide stuff like HN style comments with prev/next navigation, code windows examples that are organized into a tabbed window, a carousel style image gallery, etc...
They're all implemented in pure html+css so they're very portable between projects, but it would help to keep the ability to organize the macros (or components) in the same file.

I hear you, sometimes I do want to have multiple ones in the same file. The main reason I'm torn is that if I keep it at 1 file == 1 component, they can effectively replace shortcodes in Zola without any work. If multiples are allow, shortcodes still need to be their separate thing. The difference between macros and shortcodes is already something that confuses a lot of people when they look a bit different, so when they look the same it's going to be even worse.

So even if it is something like {{ macros::foo() }} then that would give me enough to see where a certain macro is used, etc...

Since they would require parenthesis all the time, you should just be able to grep for foo( even with the leading namespace and find all occurences.

One ask is to consider any possibility of allowing the end tags to optionally be named? When I'm visually parsing a file and I see {% end %}, it just adds extra cycles for me to recall what was the opening statement. It's not a big deal but it exists elsewhere in the tera language. In those cases I found it helpful.

Yes definitely, and it would likely be required.


@bemyak

Yes definitely, if I end up implementing this it will replace shortcodes. A shortcode would be just a component that gets some extra context from the current rendering page.

With one component per file, we would have to define the parameters and their defaults somewhere. Maybe we keep the current {% define %} approach or use a comment like jinjax but I'm not a huge fan of that. Either way it's a breaking change for the shortcodes but in exchange you would get an error if you have a typo somewhere versus potentially rendering something empty.


Another thing that was requested and which could be implemented as well is a basic type system for components (eg you can mark parameters as str/int/float/array/map/bool and it will error at runtime if types don't match.

@Keats
Copy link
Owner

Keats commented Oct 24, 2024

To take back the example from https://jinjax.scaletti.dev/guide/motivation/#/:

with the proposed changes, it would be:

{% card(label="hello") %}
    {% my_button(color="blue", shadow_size=2) %}
        {{ icon(name="ok") }} Click Me
    {% end my_button %}
{% end card %}

in Tera, which I think is acceptable considering we are not targeting just HTML so it can't be as clean as JSX from that page.
One thing to consider is an optional namespace to differentiate function calls from components, eg in the unlikely case you have a Tera function called icon, we wouldn't know what you actually want in the snippet above.
This conflict is making me wonder whether this is the right syntax though.

As for definition, if we assume one file == one component, we'll pick the component name from the file like shortcodes. The tricky bit is where to define the arguments.

The options that I can think of are:

// 1. Comments
{# arguments = (products, msg="World!") #}

// 2. Single line definition
{% define arguments = (products, msg="World!") %}

// 3. Same as currently
{% define component(products, msg="World!") %}
  The component is defined inside a "block"
{% end component %}

Option 1 is ok but highlighting might be a problem.
Option 2 is where I'm leaning currently to avoid one nesting level and also because with this option (and option 1 as well) we could let users define arbitrary things if they want to (like paths to CSS/JS in jinjax examples). They could just do {% define css = "something.css" %} and we would expose everything defined about that macro through the Rust API so someone could easily build JinjaX with Tera if they wanted.
Option 3 is what we have currently but not easy to extend like 1/2.

What do you think?

@asimpletune
Copy link
Author

Hey, this looks awesome, thank you. I really like the example above.

One thing to consider is an optional namespace to differentiate function calls from components

If the namespace is optional then I don't think having it can hurt and it can only help in the unlikely case of collisions, like you said.

This conflict is making me wonder whether this is the right syntax though.

Do you want to elaborate your thoughts on this a little bit? Are you suggesting to just use a different syntax to indicate a component, instead of an optional namespace? I'd love to hear more about what you're considering.

defining the arguments in front matter?

if we proceed with the assumption of 1:1 file and component (and component name being the filename) then why don't you define the arguments in a short little frontmatter section?

I think this might seem strange at first but it has a lot of benefits. For one thing it would solve the unnecessary nesting issue. It would also make the arguments easily readable and separate from the actual code. People can add comments and it would make documenting how a component works really easy (I do this a lot and it helps whenever I have to revisit a shortcode or macro).

I think it could also provide sort of a unified syntax for components and shortcodes. Right now, as you know, macros have kind of a macro keyword whereas shortcodes just have their templating and any variables they need are presumed to exist. The shortcode syntax in Zola now is nice because there's no nesting but it's also hard to debug. (I do defaults just by literally just setting {% set foo = foo | default(value="abc") %} everywhere).

I think it would also create a lot of forward compatibility? In the future new functionality could be added in that shortcode section and it wouldn't really crowd the component definition (like it would with the define syntax above).

I don't know, I could go on. I honestly just see mainly benefits this way and not a lot of drawbacks. What do you think @Keats and everyone else?

@asimpletune
Copy link
Author

asimpletune commented Oct 24, 2024

Just to summarize my thoughts above about using front matter for the component definition:

  1. it would work in terms of providing a place for definitions to go
  2. it could unify the syntax nicely and naturally for components and shortcodes (in zola)
  3. separates definition and implementation stuff in a nice way with lots of whitespace and room for comments
  4. forward compatibility would be good because new features and capabilities could be added in the front matter
  5. maybe it would make tooling easier to write? since one would be able to easily separate out the definition stuff from implementation.
  6. If you define the component in the front matter then you could also make the definition optional altogether, and allow people, who choose to do so, to just reference variables, which would allow people to pass in whatever they want and if it exists then great and if not then it breaks. That's how shortcodes work now anyways and it's honestly fine. That way this would simultaneously be very verbose but also very concise, if someone just wants to make a quick component without any boilerplate.

Here's an example of how it could look with front matter:

+++
[args.title]
default = "Untitled"
type = "string"

[args.count]
default = 10
type = "number"

[args.isVisible]
type = "boolean"

[args.description]  # No default or type
+++
<h1>{{ title }}</h1>
<ul>
{% for 1 in count %}
  {% if isVisible %}hello!{% endif %}
{% endfor %}
</ul>
<p>{{ description | default(value="no description yet") }}</p>

but if someone wanted to do kind of a gradual typing thing they could start with just this

<h1>{{ title }}</h1>{# this breaks if no `title` but that's ok, and event preferred, at first #}
<ul>
{% for 1 in count %}
  {% if isVisible %}hello!{% endif %}
{% endfor %}
</ul>
<p>{{ description | default(value="no description yet") }}</p>

that way they could start without any boiler plate at all, and as their work becomes more clear they could sort of progressively add types and defaults.

Thoughts?

@uncenter
Copy link

To summarize that frontmatter approach: the idea is to allow defining the arguments, with types and defaults, to a component in TOML frontmatter? I'm not sure if going with frontmatter makes sense here if this would be a built-in capability of the engine, I'd rather it be defined in the syntax of the actual template language.

The options that I can think of are:

// 1. Comments
{# arguments = (products, msg="World!") #}

// 2. Single line definition
{% define arguments = (products, msg="World!") %}

// 3. Same as currently
{% define component(products, msg="World!") %}
  The component is defined inside a "block"
{% end component %}

Option 1 is ok but highlighting might be a problem. Option 2 is where I'm leaning currently to avoid one nesting level and also because with this option (and option 1 as well) we could let users define arbitrary things if they want to (like paths to CSS/JS in jinjax examples). They could just do {% define css = "something.css" %} and we would expose everything defined about that macro through the Rust API so someone could easily build JinjaX with Tera if they wanted. Option 3 is what we have currently but not easy to extend like 1/2.

What do you think?

Highlighting would definitely be a problem for option 1, and I think reasonably so! Comments shouldn't be used for anything but... comments. Option 3 is I think what I would prefer, I don't think a file should only be able to define one component too.

@Keats
Copy link
Owner

Keats commented Oct 24, 2024

Front-matter is a no from me, way too verbose and require a TOML parser and you can't provide well integrated errors. Not to mention people will ask for YAML 5s after it lands. I would prefer having it built using Tera syntax.

Option 3 is I think what I would prefer, I don't think a file should only be able to define one component too.

How would you define additional metadata?

@uncenter
Copy link

Front-matter is a no from me, way too verbose and require a TOML parser and you can't provide well integrated errors. Not to mention people will ask for YAML 5s after it lands. I would prefer having it built using Tera syntax.

It's also confusing because other applications of Tera will probably want to use front matter (and do in the case of https://github.com/catppuccin/whiskers).

Option 3 is I think what I would prefer, I don't think a file should only be able to define one component too.

How would you define additional metadata?

Additional metadata as in types? Maybe something like this?

{% define my_component(products: str[], msg: str ="World!") %}
  ...
{% end my_component %}

@Keats
Copy link
Owner

Keats commented Oct 24, 2024

Additional metadata as in types? Maybe something like this?

I meant the CSS/JS paths from JinjaX for example. Types would look like what you wrote.

@uncenter
Copy link

{% define css = "something.css" %}

Do you mean this example? I would say reuse set_global to allow macros to expose variables to the caller, if I'm understanding correctly.

@Keats
Copy link
Owner

Keats commented Oct 24, 2024 via email

@uncenter
Copy link

Ah right I see. Sure, define would make sense as the keyword there instead of set/set_global. However I wouldn't use define again for components if that were to be the case. define in as a keyword to define static literal values doesn't make sense to also apply to dynamic components imo. Though I can't think of any other suitable keywords 😅

@Jieiku
Copy link

Jieiku commented Oct 25, 2024

This feature looks like it would be very useful, the kind of feature that after you use it a couple times you find more and more uses for it!

@Keats
Copy link
Owner

Keats commented Nov 5, 2024

Latest thoughts for calling components.

Maybe we want it different from functions just to allow namespacing in case of conflicts as in 2 components with the same name? I don't know how common that would be but since the components are global to a project, names would need to be unique. I'll need some feedback/experience on whether that actually happens. I can imagine things like a button would be different for sites and html emails for example.

  1. Call them like a function
{{ button() }}

{% button() %}
...
{% end button %}
  • Pros: clean, same syntax as Zola
  • Cons: confusing with functions, hard to add namespace if we want to
  1. Same as 1 but with a leading symbol
{{ :button() }}

{% :button() %}
...
{% end button %}

// How namespacing could look like, not needed if component name is unique
{{ :button() from "emails/button.txt" }}

Symbol can be : or :: or anything else that wouldn't be ambiguous.
Pros: no conflicts with function, easy to differentiate with functions, can add namespace easily
Cons: slightly less clean visually

  1. New delimiters
{< button() />}

{< button() >}
...
{</button>}

// How namespacing could look like, not needed if component name is unique
{< button() from "emails/button.txt" />}

I'm using {< .. >} for delimiters but it could be anything.
Pros: looks like JSX if you squint (a lot), same other benefits as 2
Cons: One more delimiter, unfamiliar for pretty much everyone in practice

The syntax for shortcodes in Zola will be changed to match whatever is chosen here so there are no more differences between shortcodes and macros/components.

Any feedback?


Another thing that I think I will miss: imports. Knowing from which file each components comes from is IMO valuable. It might not be an issue in practice, especially if you force components to be defined in specific directories. You can still grep for it but that would be the only way.

@Jieiku
Copy link

Jieiku commented Nov 5, 2024

My vote would be option 1. Call them like a function.

I do not know how important it would be to have namespaces. I vote for option 1 because it keep the syntax familiar, but if namespaces solve some problems that people are likely to run into then one of the other options would be fine, but I have no idea if option 2 or 3 would be better in that case.

Other thoughts:

To give meaningful feedback I have been trying to think of some of the best uses for this feature.

So I did a grep on all the themes:

grep --color --include=\*.html -rnw "/home/jieiku/.dev/themes" -e ".*{{.*body.*"

Surprisingly Abridge only uses the {{ body }} feature once for katex.

abridge: katex
academic-paper: figure
albatros: note, quote, table
anatole-zola: tikzjax, mermaid
apollo: note
ataraxia: blog, section
deepthought: chart, katex, mapbox, galleria, mermaid
duckquill: crt, alert
ergo: quote
even: katex
kangae: quote
karzok: hint, katex, mermaid
kita: mermaid, admonition
minimal-dark: callout, timeline, mermaid
pico: callout, timeline, mermaid, badges
polymathic: message, field, assetcard, herocard
seagull: icon_macros_dep, note
serene: detail, note, mermaid, tip, alert, warning, important, quote, codeblock, question
tabi: force_text_direction, references, mermaid, wide_container
toucan: note
tranquil: note, mermaid, tip, alert, warning, important, codeblock, question
zolarwind: diagram
zola-pickles: katex, figure

The most common use of {{body}} that I see in most shortcodes is to pass a small snippet of readable text to a shortcode.

deepthought has interesting uses of {{body}}, but they are js supported features.

Pico has an interesting use of {{body}} to construct a timeline: https://kuznetsov17.github.io/pico/features/#timeline

Seagull theme seems to already make use of the feature being discussed here??? (seems they use body by passing it as a parameter) https://git.lacontrevoie.fr/HugoTrentesaux/seagull/src/branch/master/templates/icon_macros_dep.html

Serene's codeblock shortcode allows specifying the filename, which seems like an interesting use of the feature.

With a better understanding of the practical uses of this feature I would probably be able to give much better feedback.

@Keats
Copy link
Owner

Keats commented Nov 5, 2024

With a better understanding of the practical uses of this feature I would probably be able to give much better feedback.

If you are familiar with React or component based libraries, it's basically that but in a template engine. Not necessarily only for Zola but to organise things better and make it much easier to handle complex templates. Think of a normal website where you have your inputs/buttons/images etc that you want to always be consistent. You can do some CSS class (or tailwind stuff) but it's easier if there's only one place to change it. Also nice if you are using htmx for example and want to re-render a single piece of the template. For your example in the body: it's not as needed for shortcodes as for general components. To reuse JinjaX as an example, if you go to https://jinjax.scaletti.dev/guide/slots/#/ and scroll down to composability for the modal example it's pretty obvious how much the body (or children or whatever name) will be used in practice.

There are a bunch of ways to handle it with Jinja/Django etc but it's usually handled outside of the template engine. I'm trying to figure out how to include it directly in Tera in a way that is nice to use but also extensible.

@Jieiku
Copy link

Jieiku commented Nov 5, 2024

Not necessarily only for Zola

OH! it is good that you pointed that out, not necessarily only for Zola. I actually was working on an actix-web project a few years ago that used Tera as the templating engine, and I am sure on that level this feature would be very useful! I stopped working on that project, and if I needed something more dynamic in the future I would probably use axum with Tera instead.

When your creating something like a very dynamic web application that uses Tera as the templating engine then I would probably opt to allow namespacing! so option 2 or 3.

Initially I thought I was still in the zola repo when I followed your link to this issue, I should have looked more closely!

@bemyak
Copy link

bemyak commented Nov 6, 2024

I also lean towards option 1. My intuition is that namespacing is not needed for 99% of the cases, and the rest can rename components when importing them (use <component> as <other_name>)

@Keats
Copy link
Owner

Keats commented Nov 6, 2024

With option 1 you lump together macros and function so slightly more likely to need namespacing. I personally prefer option 2 just because it's obvious if i {{ :navigation() }} that it's a component so I know I need to look only in the templates

@bemyak
Copy link

bemyak commented Nov 8, 2024

How about going the Rust way and define components with an exclamation point at the end e.g., button!()? Seems more conventional 🤷

Also, now that I'm looking at it more and more, I start liking option 3. It kinda gives you an idea that it's not just some computation/evaluation, but an actual tag-like thingie, which is exactly what component is.

@Keats
Copy link
Owner

Keats commented Nov 8, 2024

but an actual tag-like thingie, which is exactly what component is.

That's a small issue actually. You can use Tera to template anything, not just HTML. Eg with https://github.com/Keats/kickstart you can template things like Python files or really any kind of text files

@Keats
Copy link
Owner

Keats commented Nov 20, 2024

Ok this is missing some error handling/validation and actual type checking but it should be usable: #64 to play with

Current syntax:

  1. Definition
{% component navbar(greeting: string, age: integer, data: map = {})  {"css": "./array.css"} %}
{{ greeting}}: {{body}}
{% endcomponent %}

The end name and the metadata are optional

  1. Call
    Inline: {{ :navbar() }}
    With body:
{% :navbar(greeting="Hello") %}
Anything in there will be available in the component as the `body` variable
{% endcomponent :navbar %}

(The end name is optional)

The symbol before the name makes the code quite a bit simpler

Another thing missing is probably a JSX spread like feature.

@uncenter
Copy link

Thoughts on using str and int instead of string and integer to keep them consistent with the type casting filters? Or maybe change them the other way around and rename the type casting ones? Consistency would be nice :)

@Keats
Copy link
Owner

Keats commented Nov 20, 2024

Yes of course

@Keats
Copy link
Owner

Keats commented Dec 20, 2024

Has anyone tried that branch?

@asimpletune
Copy link
Author

I can take a look over the holidays

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants