Skip to content
This repository has been archived by the owner on Sep 21, 2021. It is now read-only.

Type checked templating language #118

Open
lpil opened this issue May 12, 2020 · 15 comments
Open

Type checked templating language #118

lpil opened this issue May 12, 2020 · 15 comments
Labels
area:tooling An addition to the Gleam tooling

Comments

@lpil
Copy link
Member

lpil commented May 12, 2020

pub fn my_template(name: String, scores: List(Int)) -> String =
  derive Template(file: "path/to/file")
// This template builds a HTML string, meaning that it is safe to be included into
// other html templates without being escaped. Any string values interpolated into
// this template get escaped.
pub fn my_html_template(name: String, scores: List(Int)) -> Html =
  derive HtmlTemplate(file: "path/to/file");
// in a function
let string = my_template(name: "Hello", scores: [1, 2, 3])
{% import my/helper %}

<h1>{{ name }}</h1>

<ul>
  {% for score in scores %}
    <li>{{ helper.human_readable_number(score) }}
  {% end %}
</ul>

Default escaping is HTML. A different escape method can be provided when the function is defined.

Do we support layouts?

Do we support partials/includes?

Should the file path be relative to the file? Or in templates at the base of the project? Or something else?

@nono
Copy link

nono commented May 13, 2020

By curiosity, did you consider something like jsx?

pub fn my_template(name: String, scores: List(Int)) -> HTMLTree =
  <>
    <h1>{name}</h1>
    <score_list scores={scores} />
  </>

pub fn score_list(scores: List(int)) -> HTMLTree =
  case scores {
    [] -> <p>No scores</p>
    list -> <ul><score_list_items(list) /></ul>
  }

Well, by writing the example, I can see that it will cumbersome to manipulate lists and maps like this. And I suppose that it will make the gleam grammar more complicated, with the different roles of < and >.

@eterps
Copy link

eterps commented May 13, 2020

By curiosity, did you consider something like jsx?

See also this experiment in Elm: https://github.com/pzavolinsky/elmx

@lpil
Copy link
Member Author

lpil commented May 13, 2020

JSX is only useful for HTML and is more complicated to implement compared to templates. It also performs much worse than templates unless we do a lot of optimisations.

If we're to go down the JSX route we'd need some strong benefits to counteract those drawbacks.

@nono
Copy link

nono commented May 13, 2020

To come back to templates, I think that several frameworks may want to do things a bit differently and it is a good idea to let them. For example, for layouts, would it be possible to have something like this:

// Application code
pub fn render_scores(name: String, scores: List(Int)) -> String =
  let renderer = framework.template("scores")
  renderer(name, scores)

// Framework
pub fn renderer(template: String) -> Dynamic
  fn () {
    let content = derive Template(file: strings.append("../views/", template))
    derive Template(file: "../views/layout")
  }

I don't see how to make it works with the static typing, but if we can, I think there is no need to have layouts in the language, it could be something added in userland. For partials, I think it would be the same: the framework can inject a partial function.

But, it looks like that, without macros, layout and partials should be in the templating language. If we take a syntax inspired from handlebars, it could give:

{{#import my/helper }}

{{#> my_layout }}
  {{> my_title_partial name=name}}

  <ul>
    {{#each scores}}
      <li>{{ helper.human_readable_number(_) }}
    {{/each}}
  </ul>
{{/layout}}

Default escaping is HTML. A different escape method can be provided when the function is defined.

I think it is a good idea to have escaping by default, but bypassing it can be useful on some cases (like avoiding to escape twice the content from my framework example). I know two ways to do that:

  1. having a special syntax for it (handlebars does it with {{{wont_be_escaped}}})
  2. by using a special type for string that should not be escaped.

@lpil
Copy link
Member Author

lpil commented May 13, 2020

The above code for the application and framework code won't be possible as we cannot dynamically derive values, and the paths have to be static at compile time for them to be type checked and generated by the compiler. That's why deriving has a top level syntax rather than being inside a function.

I think it is a good idea to have escaping by default, but bypassing it can be useful on some cases (like avoiding to escape twice the content from my framework example). I know two ways to do that:

Yes, good points!

One thing to consider is that we want the type system to help us avoid unsafe use of escaped values. For HTML I think this means introducing a html type, and having html templates return this type.

If the value being interpolated into the template is a Html then it is not escaped, if it is a String or Iodata then it is escaped. I've included more information in the original post.

@sporto
Copy link

sporto commented May 14, 2020

I'm wondering why this issue is relevant here. It sounds to me that at templating language should be a library implemented in userland, not in the Gleam compiler.

Sounds like you are proposing this to be a core language feature. There are many ways to do templating language, with different trade-offs, letting libraries experiment and do this could be better to settle on the best approach.

Does the language need to concern itself with this? Maybe what is needed is some way to allow libraries to run and generate Gleam code at compile time.

@lpil
Copy link
Member Author

lpil commented May 14, 2020

It's not possible to implement type safe templates in userland without a macro system, which Gleam does not have. Because of this I'm considering this suitable for inclusion in the compiler.

It's not uncommon for a templating language to be maintained by the core team of a language, examples include Elixir, Go, and Ruby.

Maybe what is needed is some way to allow libraries to run and generate Gleam code at compile time.

A macro system like this would be orders of magnitude more work, I don't expect we will be able to do this within the next couple years if at all.

@QuinnWilton
Copy link

One thing to consider is that we want the type system to help us avoid unsafe use of escaped values. For HTML I think this means introducing a html type, and having html templates return this type.

I don't know if this is the right place to talk about this, especially since it's a bit larger in scope than what you're suggesting, but you can also use the type system to model privileged information as it relates to templating. If template variables and templates are tagged with their privilege level, then you can restrict sensitive information from ever appearing in a "less sensitive" template.

Using janky and poorly modeled ML pseudocode as an example:

data PublicInformation = PublicInformation String
data PrivateInformation = PrivateInformation String

data TemplateVariable a = TemplateVariable String a

data UnprivilegedView = UnprivilegedView { template :: String
                                         , variables :: [TemplateVariable PublicInformation]}

data PrivilegedView = PrivilegedView { template :: String
                                     , publicVariables :: [TemplateVariable PublicInformation]
                                     , privateVariables :: [TemplateVariable PrivateInformation] }

data View = Privileged PrivilegedView
          | Unprivileged UnprivilegedView

Under such a type system, values need to be tagged with their security level before they can appear in a template. Certain classes of data then, such as credit card numbers, can be constructed such that the compiler prevents them from ever appearing in a public template. Obviously the code above isn't very ergonomic to work with, but I think the general idea could be made to work more nicely. In general, if you model data sensitivity with a lattice, you can prevent the flow of sensitive data into less sensitive contexts.

All that being said, it's possible that a system like this could be implemented as a library that makes use of the core templating language.

@lpil
Copy link
Member Author

lpil commented May 15, 2020

This is another interesting area to look at, though I'm not sure there is a one size fits all solution, so userland implementation is more attractive

If you've not seen it before check out Granule, they have some really interesting stuff on this area.

@CrowdHailer
Copy link

Do we support layouts?

Do we support partials/includes?

Is there any reason why they would need to be included. I wrote two libraries, EExHTML and Raxx.View separately in part to keen concepts like this out of core EExHTML, however maybe Elixir makes it easier to add them as a layer afterwards, but I would only add them if you have to, there are a few choices that need to be made for both of these features that it would be nice not to think about on the first version

@joshuacrew
Copy link

joshuacrew commented Jun 2, 2020

{% import my/helper %}

<h1>{{ name }}</h1>

<ul>
  {% for score in scores %}
    <li>{{ helper.human_readable_number(score) }}
  {% end %}
</ul>

What about a Elm or F# inspired syntax?

h1 [ ]
    [ str name ]
 ul [ ]
        (List.map (score -> li [ ] [ helper.human_readable_number(score) ]) scores)

Apologies for the terrible psuedocode

@lpil
Copy link
Member Author

lpil commented Jun 2, 2020

This is not a HTML generation system but a generic string templating system that could be used to render strings containing any content in any format. i.e. HTML, XML, plain text, yaml, toml, JavaScript, etc.

A function based HTML generation DSL could be implemented today in pure Gleam, no additional compiler features would be required :)

@Aloso
Copy link

Aloso commented Sep 7, 2020

One possibility is to use string interpolation, similar to Scala:

import my/helper

pub fn my_template(name: String, scores: List(Int)) -> String =
   html"""
   <h1>$name</h1>

   <ul>
      ${helper.each(scores, fn(score) {
       html"<li>${helper.human_readable_number(score)}</li>"
      })}
   </ul>
   """

In Scala, strings that are prefixed with an identifier are interpolated. In interpolated strings, $something and ${...} are parsed as expressions, and they are desugared (i.e. replaced at compile time) to a function call, for example:

html"<ul><li>$name</li><li>$id</li></ul>"

// is desugared to
new StringContext("<ul><li>", "</li><li>", "</li></ul>").html(name, id)

// or, in Gleam it could be desugared to
html(["<ul><li>", "</li><li>", "</li></ul>"], [name, id])

The nice thing about this is that the string interpolation is dynamic (template strings can contain any number of parameters, and can do anything with them (e.g. HTML escaping)), but the compiler can still verify that all required parameters are present. String interpolation is used pervasively in Scala, since it is very powerful and flexible.

@lpil
Copy link
Member Author

lpil commented Jan 16, 2021

With the current work on a type provider design I think this could in future be implemented in userland.

@lpil lpil transferred this issue from gleam-lang/gleam Jan 16, 2021
@lpil lpil added the area:tooling An addition to the Gleam tooling label Jan 16, 2021
@zoosky
Copy link

zoosky commented Feb 12, 2021

Maybe a Gleam wrapper around Rust Tera would be a starting point.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area:tooling An addition to the Gleam tooling
Projects
None yet
Development

No branches or pull requests

9 participants