pugjs / pug

Pug – robust, elegant, feature rich template engine for Node.js
https://pugjs.org
21.68k stars 1.95k forks source link

Re-imagining how blocks work #2055

Open ForbesLindesay opened 9 years ago

ForbesLindesay commented 9 years ago

I've been doing some thinking about how to simplify blocks in jade 2.0.0. I'd love to get people's thoughts.

jade 1.x.x

This is messy:

This means that +mixin_call, include ./filename.jade and extend ./filename.jade all do essentially the same thing. They create a "Block Context".

Examples

layout.jade

doctype html
html
  head
    title My Layout
  body
    block
    script(src="/jquery.js")
    block scripts

To use this layout template, you can use extend:

child.jade

extend ./layout.jade

h1 My Child Page

Could also have been written as:

include ./layout.jade
  h1 My Child Page

Which is, itself a short hand for:

include ./layout.jade
  block DEFAULT
    h1 My Child Page

Either way the output would be:

<!DOCTYPE html>
<html>
  <head>
    <title>My Layout</title>
  </head>
  <body>
    <h1>My Child Page</h1>
    <script src="/jquery.js"></script>
  </body>
</html>

This lets you write a neatly encapsulated component like:

dialog-component.jade

block append scripts
  script(src='/dialog.js')

div.dialog(data-dialog)
  div.dialog-heading
    block declare heading
  div.dialog-body
    block

which is equivalent to:

block append scripts
  script(src='/dialog.js')

div.dialog(data-dialog)
  div.dialog-heading
    block declare heading
  div.dialog-body
    block declare DEFAULT

It can be used as:

child-2.jade

extend ./layout.jade
  h1 My Child Page
  include ./component.jade
    block heading
      h2 My Dialog Heading
    p My Content

child-2.jade

extend ./layout.jade
  h1 My Child Page
  include ./component.jade
    block heading
      h2 My Dialog Heading
    block DEFAULT
      p My Content
<!DOCTYPE html>
<html>
  <head>
    <title>My Layout</title>
  </head>
  <body>
    <h1>My Child Page</h1>
    <div data-dialog class="dialog">
      <div class="dialog-heading">
        <h2>My Dialog Heading</h2>
      </div>
      <div class="dialog-body">
        <p>My Content</p>
      </div>
    </div>
    <script src="/jquery.js"></script>
    <script src="/dialog.js"></script>
  </body>
</html>
TimothyGu commented 9 years ago

In general the idea sounds okay.

  • A new keyword is added to JavaScript inside jade called block that must be immediately followed by a block name. e.g. input(value=(block value))

This sounds messy...

And I assume that you can still add a block default like

block declare content
  p Hey!

?

ghost commented 9 years ago

Just came across this and it sounds promising - it's not clear to me whether it would allow you to have multi-level inheritance without specifying a different name for each inner block, but if so that would be brilliant. One of the main use cases I have is a main layout, and then most content included within a div with some padding, but not all of them, so to have some pages be main <- padding div <- content, and some just main <- content, without worrying about block names, would be so much cleaner.

alubbe commented 9 years ago

Sounds like a great way to consolidate mixins, includes and extends. When you say "Block Context", how does that interact with local JS variables?

ForbesLindesay commented 9 years ago

I assume that you can still add a block default

Absolutely, you can also do the same thing for the default block:

block
  p Default content

[new JavaScript keyword] sounds messy

maybe, the alternative is to just use the block function like we already do inside mixins. One of the advantages of using a keyword is that we can guarantee that we will be able to statically analyse the location of all calls to request a block. We could just only support calling block with a string literal name though, which would serve the same purpose. The semantics would be a bit like:

- var x = block('my-block')

is approximately equivalent to (but we'd hopefully find a nicer way to implement it):

- var old_buffer = buf;
block declare my-block
- var x = buf; buf = old_buffer;

would [it] allow you to have multi-level inheritance without specifying a different name for each inner block

Yes, absolutely. This has long been a goal for 2.0.0, and could be implemented with or without this change. It's made achievable by splitting out the linker as a separate component of the compiler pipeline.

how does ["Block Context"] interact with local JS variables?

I'm not sure. I think we need another whole discussion about variable context. We probably need a formal way of marking variables/mixins as local vs. global vs. exposed to the parent's Block Context. Probably a whole separate discussion.

mikeyhew commented 9 years ago

Here's my feedback:

  1. I never really used yield, and don't really know what it does, so can't comment.
  2. Definitely keep the extends for extending layouts, etc. because it's such a common use case.
  3. I like the idea of allowing named blocks to be passed to include and mixins.
  4. This block declare stuff confuses me: what's the point of it? I don't think I like it. The example you gave could be written as follows, without the word 'declare':

    div.dialog(data-dialog)
     div.dialog-heading
       block heading
     div.dialog-body
       block
  5. This is the first I've heard of block prepend and block append - they look useful!
  6. I definitely like the idea you proposed for anonymous blocks, so that it's just syntactic sugar for declaring the DEFAULT block. Good on you!
mikeyhew commented 9 years ago

A couple more questions:

EDIT: extend and extends

TimothyGu commented 9 years ago

@mikeyhew said:

Are extends and extends both keywords? What does each of them do?

Huh?

TimothyGu commented 9 years ago

@mikeyhew said:

What would be the return value of block foo in JavaScript? Wouldn't it be better if you could just access blocks via a variable (ie: blocks.foo), instead of with a keyword?

It's not that simple. Consider this case:

mixin a
  block

+a
  - var b = 0
  p= b

The block has to be a function.

TimothyGu commented 9 years ago

@mikeyhew said:

The example you gave could be written as follows, without the word 'declare'

This I have to agree with. I don't see the point in requiring the user to put the extraneous declare, especially when declaring the default block one does not have to put declare.

mikeyhew commented 9 years ago

@TimothyGu regarding the value of the expression block foo: OK, so the value of block foo is a function then, that presumably returns a string. Why would block need to be a keyword?

@TimothyGu said:

This I have to agree with. I don't see the point in requiring the user to put the extraneous declare, especially when declaring the default block one does not have to put declare.

@ForbesLindesay what do you think? Is there anything we're missing, or can the block declare syntax be removed?

@TimothyGu said:

Huh?

Huh what? Please elaborate.

ForbesLindesay commented 9 years ago

The value of block foo would be a string. But block foo may potentially have side effects, and may potentially be computationally expensive. As such, it should be evaluated lazily. The reason we may want to make it a keyword, is that by making it a keyword, we can use static analysis to find all usages of a given block. This may lead to a lot of extra performance optimisation opportunities in the future.

The problem I'm attempting to solve with the declare syntax, is that we currently guess at whether you intended to declare, or replace a block when we see the block keyword. e.g.

extends layout.jade

//- this means "replace content", but it also means "declare content" if another template extends this one.
block content
  div.body
    //- this means "declare body"
    block body

If we unify the block keyword, then what does this mean:

//- layout.jade

mixin foo
  .modal
    .heading
      block heading
    .body
      block body

+foo
  block heading
    h1 My Heading
  block body
    h1 My Body

//- child.jade

extends ./layout.jade

block heading
  h1 My New Heading
block body
  h1 My New Body

The blocks in the mixin call could mean "replace the block inside the mixin" or "declare the block for when this template is extended". In this example, I've used them to mean both.

Huh?

I assume you meant to ask whether extend and extends are both keywords. They are, but they are synonyms.

viktorbezdek commented 9 years ago

I'm sorry I still didn't posted my contribution - crazy times at work. Not enough brainpower in the evenings.

Here's few notes about what I'm trying to solve.

Statements declare and append/prepend are little too descriptive compared to rest of Jade, but the solution at all makes sense. I'm looking for ways to simplify the syntax while maintaining readability and clarity.

Usage of mixins is perfect example and reference solution I'd like to find for blocks

//- mixin declaration
mixin whatever()
    p I'm a mixin.

//- usage
+whatever

We're using keyword mixin for declaration and + add sign for usage. Everything is crystal clear.

Example with blocks

mixin whatever()
    block header
    .content
       block //- shorthand for block DEFAULT

//- usage
+whatever
    @header
        h1 some header
     //- synonym for @default
    @block
        p block content

Usage is consistent with mixins, and @ at sign is descriptive enough to make it clear. block keyword is used only for declaration.

I'm looking for ways how to make the append/prepend keyword work with this. Nothing better than @- or @+ came to my mind and I don't like this.

Does this proposal make sense to you? Any ideas?

Aratramba commented 9 years ago

how about

block set whatever
  | whatever

It's a bit shorter than block declare foo and means pretty much the same thing, right? Looks prettier imo.

@viktorbezdek I'm not sure if @- and @+ make for the nicest syntax. I do like the fact that these calls are as short as +mixin, though I don't think shorter always means more readable (especially in longer, more complex files). I think I just like how the word block is explicitly used wherever a block is involved.

If you go this road with at-signs, you might end up using signs for includes, extends etc. Might look like this

*./ext.jade

mixin whatever
  block whatever
    | #{whatever}
    &./inc.jade
    :markdown
      # Markdown

+whatever
  @+whatever
    | whatever
vai commented 9 years ago

How about -

    block doot
      h1 Doot

    // elsewhere ...
    doot:
      | replaces

    // or doot:after ?
    doot:prepend
      | prended ...

terminal-colon isn't an expression so far as I know?

I don't really like the +/- apart from brevity - 'subtract' is not really the semantics of prepend.

viktorbezdek commented 9 years ago

@val terminal colon is used when you're using two tags on same line

em: strong Bold and italic text
vai commented 9 years ago

That's highly annoying, mostly because I remember that feature existing, trying to use it, and it failing, so I thought it had been removed. Was it missing for a few versions?

jeromew commented 9 years ago

I share some of the expressed fears on the "declare" notation which - I don't really know why - does not feel natural compared with the overal jade syntax.

I don't know if this can lead somewhere or will ring some bells among contributors, but this problem makes me think of "aspect oriented programming" where people have tried to find a notation to modify the execution of a program :

append, prepend also seem close to LISP modifiers before, after and around

block exist in some way, to override the html just like AOP would inject aspects in a program.

for some explanation on AOP, you can read https://en.wikipedia.org/wiki/Aspect-oriented_programming

One of the interesting thing is that AOP defines "primitive pointcut designators (PCDs)." which are expressions that are used to locate specific points in the execution flow of a program. You can find an example on the wikipedia page.

In jade, there could be PCDs that define specific parts of the layout

for exemple, we could have

// definition of the headerscript location
html
  head
    div#headerscript

// definition of the script PCD
pointcut script() : #headerscript

// append
after() : script()
  script(src='/dialog.js')

the notation would need tweeking but this way, we would have a way to target things that the compiler needs to modify at compile time, without being limited to the block notation.

A pointcut could also target things like "all A tags", "just before the end of body", "all DIV inside layout.jade".

ForbesLindesay commented 9 years ago

@viktorbezdek While I agree that block declare foo might be too verbose, and definitely doesn't read well, the trend I have been trying to push for is fewer syntactic shorthands rather than more.

We want syntactic shorthands where you can guess what the shorthand does based on similar usage in other parts of the file (e.g. ! always means unescaped, # followed by an open bracket starts some sort of interpolation, the right hand side of = is always a JavaScript expression). We also want syntactic shorthands for things that you do extremely frequently in well designed jade templates (e.g. calling a mixin via +).

An example where a shorthand is counter-productive, is using the syntax !!! 5 as a shorthand for doctype html. Nobody will need to look the latter up in our docs, whereas everyone will have to look up what !!! means the first time they see it.

Well designed jade templates should only have a handful of blocks per file, so I don't think they merit such extreme short hands. On the other hand, I do think we could come up with something a little more succinct using @ as a convenient shorthand (see my "Alternative Syntax Proposal")

@Aratramba Using something like set/replace on the overriding side would be an alternative to using declare on the declaration side. I don't know that it offers a huge advantage, but it's a possibility.

@jeromew Thanks for this comparison with AOP. I think the idea of a pointcut is very close to what we are trying to do with jade blocks. I suspect the terminology of block will be more immediately accessible than pointcut to our users, although I could well be wrong as it's just my intuition. I don't think it makes sense to radically change the terminology here. I also think it's probably prefereable to keep the extension points limited to explicit declarations, rather than allowing things like injecting code after all A tags. I think allowing arbitrary extension will lead to a very brittle experience when editing layout templates. I was thinking that block append foo could append to all foo blocks and that you could declare a block with the same name multiple times in a given file. This would let you do things like:

if ajax
  block content
else
  doctype html
  html
    head
      title My Page Title
      block styles
    body
      block content
      block scripts

Alternative Syntax Proposal

Core syntax:

Shorthands:

Bikeshedding

Please focus for now on the functionality, rather than the naming of these keywords, but here are some possible alternatives:

Examples

layout.jade

doctype html
html
  head
    title My Layout
  body
    @block
    script(src="/jquery.js")
    @block scripts

To use this layout template, you can use extend:

child.jade

extend ./layout.jade

h1 My Child Page

Could also have been written as:

include ./layout.jade
  h1 My Child Page

Which is, itself a short hand for:

include ./layout.jade
  @set DEFAULT
    h1 My Child Page

Either way the output would be:

<!DOCTYPE html>
<html>
  <head>
    <title>My Layout</title>
  </head>
  <body>
    <h1>My Child Page</h1>
    <script src="/jquery.js"></script>
  </body>
</html>

This lets you write a neatly encapsulated component like:

dialog-component.jade

@append scripts
  script(src='/dialog.js')

div.dialog(data-dialog)
  div.dialog-heading
    @block heading
  div.dialog-body
    @block

which is equivalent to:

@append scripts
  script(src='/dialog.js')

div.dialog(data-dialog)
  div.dialog-heading
    @block heading
  div.dialog-body
    @block DEFAULT

It can be used as:

child-2.jade

extend ./layout.jade
  h1 My Child Page
  include ./component.jade
    @set heading
      h2 My Dialog Heading
    p My Content

child-2.jade

extend ./layout.jade
  h1 My Child Page
  include ./component.jade
    @set heading
      h2 My Dialog Heading
    @set DEFAULT
      p My Content
<!DOCTYPE html>
<html>
  <head>
    <title>My Layout</title>
  </head>
  <body>
    <h1>My Child Page</h1>
    <div data-dialog class="dialog">
      <div class="dialog-heading">
        <h2>My Dialog Heading</h2>
      </div>
      <div class="dialog-body">
        <p>My Content</p>
      </div>
    </div>
    <script src="/jquery.js"></script>
    <script src="/dialog.js"></script>
  </body>
</html>
vai commented 9 years ago

absolutely in favour of everything in that proposal. It solves several orthogonal use cases, and encourages composition.

Aratramba commented 9 years ago

I certainly see the benefits of having include, extend and mixin create a block context. That's great.

I think the @block syntax might take a bit of getting used to, but given good syntax highlighting I think this could make the document pretty readable. I'm pretty sure I will be writing @block DEFAULT a lot though, since it removes a bit of magic while scanning a document.

We also want syntactic shorthands for things that you do extremely frequently in well designed jade templates

Absolutely.

I don't think it makes sense to radically change the terminology here.

Absolutely 2.

bikeshedding

ohjimijimijimi commented 9 years ago

I just posted a feature request that maybe can be taken into consideration here: https://github.com/jadejs/jade/issues/2127

franciscop commented 9 years ago

Another take might be to declare where the parent content goes:

div.card
  header
    block header
      h2 Cool title

That was the same as 1.x, now let's check the other side:

extends ./card.jade

// replace
@header
  h2 Much cooler title

// Append
@header
  @parent
  button.close x

// Prepend
@header
  button.back back
  @parent

// Any other combination
@header
  div.wrapper
    @parent

So my proposal is, instead of explicitly declaring a prepend or append which might need more workarounds in the future (as "wrap"), to be able to use the parent value. I am not completely happy with the previous syntax, maybe something like this is better:

include ./card.jade
  @header
    extends @header
    button.close x

This would also allow extending, for example, on buttons:

// buttons.jade
div.buttons
  block buttons
    button.like Like

// buttons_user.jade
extend ./buttons.jade
  @buttons
    extends @buttons
    button.edit Edit

// buttons_admin.jade (has 3 buttons)
extend ./buttons_user.jade
  @buttons
    extends @buttons
    button.delete Delete
Adirio commented 8 years ago

Another take might be to declare where the parent content goes: [...] So my proposal is, instead of explicitly declaring a prepend or append which might need more workarounds in the future (as "wrap"), to be able to use the parent value.

Your notation would mean that you can't create a block with a name of parent. Merging the parent idea (which and also be called inherited or something similar:

Alternative Syntax Proposal v2

Core syntax:

@block/let/declare <BLOCK NAME>: declares a named block to be overridden elsewhere. @set/replace <BLOCK NAME>: override the content of a declared block. @parent/inherited: when called from inside a @set/replace <BLOCK NAME>inserts the content of that block previous to this override. Shorthands: @block === @block/let/declare DEFAULT content of a mixin call/extending template/include call expands to be inside @set/replace DEFAULT.

Examples

Simgle block declaration

layout.jade

doctype html
html
  head
    title My Layout
  body
    @block
    script(src="/jquery.js")

To use this layout template, you can use extends:

extends ./layout.jade
h1 My Child Page

or include:

include ./layout.jade
  h1 My Child Page

HTML generated by both:

<!DOCTYPE html>
<html>
  <head>
    <title>My Layout</title>
  </head>
  <body>
    <h1>My Child Page</h1>
    <script src="/jquery.js"></script>
  </body>
</html>

Multiple block declaration

layout.jade

doctype html
html
  head
    title My Layout
  body
    @block
    script(src="/jquery.js")

dialog-component.jade

div.dialog(data-dialog)
  div.dialog-heading
    @block/let/declare heading
  div.dialog-body
    @block

It can be used as: child.jade

extends ./layout.jade
h1 My Child Page
include ./component.jade
  @set heading
    h2 My Dialog Heading
  p My Content

HTML generated:

<!DOCTYPE html>
<html>
  <head>
    <title>My Layout</title>
  </head>
  <body>
    <h1>My Child Page</h1>
    <div data-dialog class="dialog">
      <div class="dialog-heading">
        <h2>My Dialog Heading</h2>
      </div>
      <div class="dialog-body">
        <p>My Content</p>
      </div>
    </div>
    <script src="/jquery.js"></script>
  </body>
</html>

Overriding, prepending, appending and wrapping

layout.jade

doctype html
html
  head
    title My Layout
    @block/let/declare styles
  body
    @block
    @block/let/declare scripts

child1.jade

extends ./layout.jade
h1 My Child Page
@set/replace styles
  link(href='/css/style1.css', rel='stylesheet')
@set/replace scripts
  script(src='/js/jquery.js')

child2.jade

extends ./child1.jade
@parent/inherited
h2 My Child Subtitle
//- Override
@set/replace styles
  link(href='/css/stile2.css', rel='stylesheet')
//- Prepend
@set/replace styles
  link(href='/css/base.css', rel='stylesheet')
  @parent/inherited
//- Append
@set/replace scripts
  @parent/inherited
  script(src='/js/my_script.js')
//- Wrap
@set/replace scripts
  // Scripts
  @parent/inherited
  // Scripts

HTML generated: child1:

<!DOCTYPE html>
<html>
  <head>
    <title>My Layout</title>
    <link href="/css/stile1.css" rel="stylesheet">
  </head>
  <body>
    <h1>My Child Page</h1>
    <script src="/js/jquery.js"></script>
  </body>
</html>

child2:

<!DOCTYPE html>
<html>
  <head>
    <title>My Layout</title>
    <link href="/css/base.css" rel="stylesheet">
    <link href="/css/stile2.css" rel="stylesheet">
  </head>
  <body>
    <h1>My Child Page</h1>
    <h2>My Child Subtitle</h2>
    <!-- Scripts -->
    <script src="/js/jquery.js"></script>
    <script src="/js/my_script.js"></script>
    <!-- Scripts -->
  </body>
</html>
smaudet commented 8 years ago

I'm reading through these...my specific case is that I wanted to include a folder of md files, which all need to be wrapped by various markup. I didn't want to have to build my own rendering pipeline, although it appears evident that with jade 1.x that is the only way forward. Jade 2.x may alleviate this, or Jade 3.x even.

I suppose what makes the most sense for me is to go ahead and build said rendering pipeline, then see about porting it to jade, since I'll need something similar, and I will want to think about things like ordering (I know this is a concern which has been brought up before).

I think in my ideal world I can do something like this:

mixin fancy_block(content)
   block blah
    include content

block blah
  h3 Header
  section

p Hello World!
  each thing in blocks
    +fancy_block(thing)

And I'll get something like:

p Hello World!
  h3 Header
  section
    <insert content here>
  h3 Header
  section
    <insert content here>