Skip to content
This repository

Make mixin blocks act like blocks #600

Closed
wants to merge 9 commits into from

3 participants

Cameron Howey Roman Komarov TJ Holowaychuk
Cameron Howey

This removes the contents keyword, and adds the block keyword:

  • contents was a string, is now removed

  • block is a function, is called whenever the block statement is used within a mixin

  • Because block is a function, it is called "in-context" and can therefore render prettily

  • block can be tested for truthy-ness, e.g. if (block) works as expected

added some commits April 22, 2012
Cameron Howey Make mixin blocks act like blocks
This removes the `contents` keyword, and adds the `block` keyword:

* `contents` was a string, is now removed

* `block` is a function, is called whenever the `block` statement is used within a mixin

* Because `block` is a function, it is called "in-context" and can therefore render prettily
cf886ec
Cameron Howey Update help file eec0d00
Cameron Howey

I hijacked the block keyword for mixins. This may not be the best keyword?

I did more thorough testing for this pull request. I tried many combinations of indents and mixins, and this correctly renders them with {pretty: true}.

I don't like how block is a function and can be accessed directly by the user as a variable. This makes the following work, because block.toString() is implicitly called, but it sure looks ugly:

mixin foo
  p= block
Roman Komarov
kizu commented April 23, 2012

New syntax of mixins/blocks is great!

However, I stumbled with this case: I want a mixin that can have arguments, but can be without them. And if I do something like that:

mixin foo(bar)
  h2 the block
  p
    if block
      block
    else
      | no block

  h2 the bar
  p
    if bar
      =bar
    else
      | no bar

h1 Mixin with an argument
+foo("hey!")
  | I'm a content!

h1 Mixin without an argument

+foo
  | I'm a content!

Then if I call the mixin with an argument it's all ok, but if I'll call the mixin without an argument, I'll get some strangeness:

<h1>Mixin without an argument</h1>
<h2>the block</h2>
<p>no block
</p>
<h2>the bar</h2>
<p>function (){
__jade.unshift({ lineno: 19, filename: __jade[0].filename });
__jade.unshift({ lineno: 19, filename: __jade[0].filename });
buf.push('I\'m a content!');
__jade.shift();
__jade.shift();
}
</p>

There would be no block and the bar would become the content.

Roman Komarov
kizu commented April 23, 2012

Also! How hard it would be to make Mixins work like normal tags? So we could do

+foo Some content

or

+foo.
  Some content

That would be awesome!

Cameron Howey

Mixins without the parentheses is easy to do, so long as you are still passing arguments like function arguments. So this could be made to work:

mixin foo(bar, baz)
  p= bar
  p= baz

+foo "Some content", "Some other content"

I guess you want it so that if a mixin is missing parentheses after it, then it should act just like one big text string? And this would be the first arg?

mixin center(stuff)
  div.centered= stuff

+center Some content

This change would be beyond the scope of this commit.

added some commits April 23, 2012
Cameron Howey Remove `mixing` arg which is already covered by `parentIndents` 8ea4cd0
Cameron Howey Pass mixin blocks as `this`
By using `Function.call`, we can pass mixin blocks without affecting arguments, which otherwise leads to weird bugs if optional arguments are being used in the mixin.

This also makes the `block` variable true/null instead of returning the function itself, which is safer for users.  The function is instead available as `this.block`.
3f7b2a7
Cameron Howey

Okay, this should fix your first bug, anyway. Let me know if it works.

The second point should be its separate issue since it won't be part of this commit. (This commit isn't even accepted yet.)

Roman Komarov
kizu commented April 23, 2012

Yep, the fix works, thanks!

The second part is a bit different — I think that the part after the space must be the just like the text block, not like the argument.

It would be supercool if the mixins could work just like a normal tag, so this:

+foo bar

would be equal to

+foo
  | bar

And in more complex cases it could still behave like the simple tag: so the following would be possible:

+foo= someVariable

+foo: .bar: +foo("baz")

etc. However, I agree that it isn't in this scope, so I'll fill it later as a separate issue.


Thanks again, the mixins in Jade are becoming better and better, I really looking forward to using them :)

Cameron Howey

@visionmedia could you review? There seems to be no performance impact. It passes all tests.

I think using mixin.call(contextObject, ...) instead of mixin(...) opens up some possibilities, since there can be extra jade-only goodies provided for mixins that would not be available for normal functions.

added some commits April 23, 2012
Cameron Howey Update test case
(The "block" property automatically propogates up the chain through mixins.)
5dd513a
Cameron Howey Removed code that no longer is needed 4c1a9a8
Cameron Howey Add support for mixins "working like normal tags"
Any text after the parentheses acts like a text node.

It can be accessed with the `block` keyword.
22f37d6
Cameron Howey

@kizu, I was able to add the "work like normal tags" style of mixin.

How it works is like so:

mixin foo(bar)
  - if (!bar) bar = 'centered'
  div(class=bar)
    block

body
  +foo Hello
  +foo("left-align") World!
  +foo
    p Some content

which renders like

<body>
  <div class="centered">Hello
  </div>
  <div class="left-align">World!
  </div>
  <div class="centered">
    <p>Some content</p>
  </div>
</body>

So the text you put after the mixin, after the parentheses (which are optional), act like a text node which goes where your block statement goes.

Also, any additional block statement just gets appended.

This is the most intuitive and straightforward way I could think to implement this.

It doesn't support the class and id syntax for normal tags, but it is a start.

Roman Komarov
kizu commented April 24, 2012

@chowey Wow, cool!

BTW, what if there'd be an arguments variable like in Stylus?

So you could just do

mixin foo
  div(arguments)
    block

+foo(class="baz")
  p Some content

And if you'd support the class and id as it works now for tags, it would become even this:

mixin foo
  div(arguments)
    block

+foo.baz
  p Some content

Also, if there would be a way to access some named/special arguments, you could do this:

mixin item
  li.menu-item(class=arguments.class)
    a(href=arguments.href)
      block

ul.menu
  +item.current(href="/foo/") Foo 
  +item(href="/bar/") Bar
  +item(href="/baz/") Baz

…And many more powerful mixins like this.

Cameron Howey

The problem with attributes is that the syntax is a bit ambiguous, because parentheses are also used for passing arguments.

I guess we could require explicit = in attribute declarations. So you could do foo(checked=true) for an attribute and foo(true) for an argument.

I would not use the arguments variable like in Stylus, because arguments is already a javascript reserved word and someone may want to access it. I would use attributes or attrs something.

Cameron Howey Make `mixin` parse like `tag`
(Arguments for the mixin MUST come first)
41f495f
Roman Komarov
kizu commented April 24, 2012

The exact syntax doesn't matter, so attributes or attrs would be great to.

Cameron Howey

Okay, here is preliminary support for attributes. You must pass arguments to the mixin first, then attributes after that. Otherwise it should work just like a tag.

+foo(arg1, arg2)#id.class1.class2(href="www.foo.com") Text
  p More content

They are accessed with attributes.id, attributes.href, etc.

There is no support for div(attributes) within the mixin doing anything meaningful.

TJ Holowaychuk
Owner

shit, I have 150+ notifications I didn't see this one and I implemented block this morning

TJ Holowaychuk
Owner

as for classes/ids im not sure how I feel about those with mixins

Roman Komarov
kizu commented April 25, 2012

@chowey Wow, so awesome already! Some minor issues:

If you have a mixin like this:

mixin bar
  .bar(class=attributes.class)
    block

+bar baz

then you'd have this render:

<div class="bar undefined">baz
</div>

So if the mixin have the some class and have the attributes.class, that is not specified on the mixin call, it would render undefined class.

Also, is there a way to throw all the attributes at once? Like

mixin bar
  .bar(attributes)
    block

so all the passed attributes would go to defined block. It can be useful if I would like to create a mixin that would add some wrappers to block, so if I need some extras in every link, I could do

mixin link
  span.link-wrapper
    a.link(attributes)
      span.link-inner

+link.current no link here
+link(href="#baz") baz
+link.external(href="#bar") bar

And that would give me the desired code. Now I need to write all the attributes manually here like

a.link(class=attributes.class, href=attributes.href, target=attributes.target)

@visionmedia classes&ids, as attributes, can be really useful in mixins when using them for blocks/objects in smacss/bem-like markup. So you could create a mixin for some block and then easily adjust it based on classes, easily add id's or anything to it. It's just a shortcut, but can be very handy.

Also, look at the examples that I provided earlier — all of them would be much uglier if we'd make them without such syntax, all of those things would go in arguments etc. If you need more examples how this can be useful — just tell me and I'll provide them :)

Cameron Howey

Okay @visionmedia you want to ditch this change, or what?

I was actually just trying to get mixin blocks to print pretty. That's really all I was doing with this pull request to start with. In order to do that, you need the function-style of block like I did. The simple string version will always print with no indents.

I can roll this back so that there is no attribute support. Or split that to a different pull request. But I do think you should consider using the function-style of block.

Give me some guidance and I will help!

TJ Holowaychuk
Owner

we should definitely separate things into different PRs, we can do block == != block() with that thing I pushed, I forgot we were going to defer for pretty-printing

TJ Holowaychuk visionmedia closed this April 25, 2012
Cameron Howey chowey referenced this pull request April 25, 2012
Merged

Patch block #608

Cameron Howey chowey referenced this pull request April 25, 2012
Closed

Mixin `attributes` #609

TJ Holowaychuk
Owner

I like it in some instances but I also don't want to reinvent regular javascript arguments for mixins in some sense, especially since this adds a reasonable amount of complexity that needs to be maintained. I think we need to visit some concrete use-cases. I like the look of it for things like +item(href='foo'), and things like .form(method='get', action='foo'). One thing I really don't like is trying to disambiguate between "regular" js args and this form of args. I'll definitely have to think on this one for a bit and maybe try out some examples

Cameron Howey

Fair enough.

My thought was that we want to disambiguate between js args and mixin-specific keywords. They should be conceptually separate. To the user the mixin-specific keywords should just work, without him having to worry about the javascript behind it.

The real reason I did it though is because I couldn't see a better way to handle a use case like this:

mixin foo(bar)
  - if (bar)
    h1= bar
  block

+foo("Title")
  p Some content
+foo
  p Some more content

In this example, when block is passed as an argument, if (bar) will always be truthy since bar will get the block variable if bar is omitted from the mixin call. Further, block becomes unavailable.

TJ Holowaychuk
Owner

wouldn't your .call() change fix that? I'm just talking attributes for mixins

Cameron Howey

Oh, yes.

Currently my test is this regex in the Lexer: /^ *[-\w]+ *=/. It tests this against whatever is in the first pair of parentheses. If this regex tests false, then it assumes you are passing arguments.

This should only fail if you try to use attributes like +inputBox(checked) which would assume that checked is an argument.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 9 unique commits by 1 author.

Apr 22, 2012
Cameron Howey Make mixin blocks act like blocks
This removes the `contents` keyword, and adds the `block` keyword:

* `contents` was a string, is now removed

* `block` is a function, is called whenever the `block` statement is used within a mixin

* Because `block` is a function, it is called "in-context" and can therefore render prettily
cf886ec
Cameron Howey Update help file eec0d00
Apr 23, 2012
Cameron Howey Remove `mixing` arg which is already covered by `parentIndents` 8ea4cd0
Cameron Howey Pass mixin blocks as `this`
By using `Function.call`, we can pass mixin blocks without affecting arguments, which otherwise leads to weird bugs if optional arguments are being used in the mixin.

This also makes the `block` variable true/null instead of returning the function itself, which is safer for users.  The function is instead available as `this.block`.
3f7b2a7
Cameron Howey Update test case
(The "block" property automatically propogates up the chain through mixins.)
5dd513a
Cameron Howey Removed code that no longer is needed 4c1a9a8
Apr 24, 2012
Cameron Howey Add support for mixins "working like normal tags"
Any text after the parentheses acts like a text node.

It can be accessed with the `block` keyword.
22f37d6
Cameron Howey Make `mixin` parse like `tag`
(Arguments for the mixin MUST come first)
41f495f
Apr 25, 2012
Cameron Howey Make `attributes` object available to mixins 083215b
This page is out of date. Refresh to see the latest.
6  jade.md
Source Rendered
@@ -399,7 +399,7 @@
399 399
        <p>Hello Tobi</p>
400 400
 
401 401
   Mixins may optionally take blocks, when a block is passed
402  
-  its contents becomes the implicit `content` argument. For
  402
+  its contents becomes the implicit `block` argument. For
403 403
   example here is a mixin passed a block, and also invoked
404 404
   without passing a block:
405 405
 
@@ -407,8 +407,8 @@
407 407
         .article
408 408
           .article-wrapper
409 409
             h1= title
410  
-            if content
411  
-              != content
  410
+            if block
  411
+              block
412 412
             else
413 413
               p No content provided
414 414
       
91  lib/compiler.js
@@ -233,6 +233,14 @@ Compiler.prototype = {
233 233
       , escape = this.escape
234 234
       , pp = this.pp
235 235
     
  236
+    // Block keyword has a special meaning in mixins
  237
+    if (this.parentIndents && block.mode) {
  238
+      if (pp) this.buf.push("__indent.push('" + Array(this.indents + 1).join('  ') + "');")
  239
+      this.buf.push('block && this.block();');
  240
+      if (pp) this.buf.push("__indent.pop();")
  241
+      return;
  242
+    }
  243
+    
236 244
     // Pretty print multi-line text
237 245
     if (pp && len > 1 && !escape && block.nodes[0].isText && block.nodes[1].isText)
238 246
       this.prettyIndent(1, true);
@@ -277,32 +285,70 @@ Compiler.prototype = {
277 285
 
278 286
   visitMixin: function(mixin){
279 287
     var name = mixin.name.replace(/-/g, '_') + '_mixin'
280  
-      , args = mixin.args || '';
  288
+      , args = mixin.args || ''
  289
+      , block = mixin.block
  290
+      , attrs = mixin.attrs
  291
+      , pp = this.pp;
281 292
 
282 293
     if (mixin.call) {
283  
-      if (this.pp) this.buf.push("__indent.push('" + Array(this.indents + 1).join('  ') + "');")
284  
-      if (mixin.block) {
285  
-        if (args) {
286  
-          this.buf.push(name + '(' + args + ', function(){');
287  
-        } else {
288  
-          this.buf.push(name + '(function(){');
289  
-        }
290  
-        this.buf.push('var buf = [];');
  294
+      var classes = []
  295
+        , escaped = {};
  296
+      
  297
+      if (pp) this.buf.push("__indent.push('" + Array(this.indents + 1).join('  ') + "');")
  298
+      
  299
+      if (block) {
  300
+        this.buf.push(name + '.call({block: function(){');
  301
+        
  302
+        // Render block with no indents, dynamically added when rendered
  303
+        this.parentIndents++;
  304
+        var _indents = this.indents;
  305
+        this.indents = 0;
291 306
         this.visit(mixin.block);
292  
-        this.buf.push('return buf.join("");');
293  
-        this.buf.push('}());\n');
  307
+        this.indents = _indents;
  308
+        this.parentIndents--;
  309
+        
  310
+        this.buf.push('},');
294 311
       } else {
295  
-        this.buf.push(name + '(' + args + ');');
  312
+        this.buf.push(name + '.call({block: this.block,');
296 313
       }
297  
-      if (this.pp) this.buf.push("__indent.pop();")
  314
+      
  315
+      if (attrs.length) {
  316
+        this.buf.push('attributes: {')
  317
+        
  318
+        var buf = [];
  319
+        attrs.forEach(function(attr){
  320
+          escaped[attr.name] = attr.escaped;
  321
+          if (attr.name == 'class') {
  322
+            classes.push('(' + attr.val + ')');
  323
+          } else {
  324
+            var pair = "'" + attr.name + "':(" + attr.val + ')';
  325
+            buf.push(pair);
  326
+          }
  327
+        });
  328
+
  329
+        if (classes.length) {
  330
+          classes = classes.join(" + ' ' + ");
  331
+          buf.push("class: " + classes);
  332
+        }
  333
+        
  334
+        this.buf.push(buf.join(', ') + '}');
  335
+      } else {
  336
+        this.buf.push('attributes: this.attributes');
  337
+      }
  338
+      
  339
+      if (args) {
  340
+        this.buf.push('}, ' + args + ');');
  341
+      } else {
  342
+        this.buf.push('});');
  343
+      }
  344
+      
  345
+      if (pp) this.buf.push("__indent.pop();")
298 346
     } else {
299  
-      args = args
300  
-        ? args.split(/ *, */).concat('content').join(', ')
301  
-        : 'content';
302 347
       this.buf.push('var ' + name + ' = function(' + args + '){');
303  
-      if (this.pp) this.parentIndents++;
  348
+      this.buf.push('var block = !!this.block, attributes = this.attributes || {};');
  349
+      this.parentIndents++;
304 350
       this.visit(mixin.block);
305  
-      if (this.pp) this.parentIndents--;
  351
+      this.parentIndents--;
306 352
       this.buf.push('}');
307 353
     }
308 354
   },
@@ -317,7 +363,8 @@ Compiler.prototype = {
317 363
 
318 364
   visitTag: function(tag){
319 365
     this.indents++;
320  
-    var name = tag.name;
  366
+    var name = tag.name,
  367
+        printIndent = this.pp && !tag.isInline();
321 368
 
322 369
     if (!this.hasCompiledTag) {
323 370
       if (!this.hasCompiledDoctype && 'html' == name) {
@@ -327,9 +374,8 @@ Compiler.prototype = {
327 374
     }
328 375
 
329 376
     // pretty print
330  
-    if (this.pp && !tag.isInline()) {
  377
+    if (printIndent)
331 378
       this.prettyIndent(0, true);
332  
-    }
333 379
 
334 380
     if (~selfClosing.indexOf(name) && !this.xml) {
335 381
       this.buffer('<' + name);
@@ -351,9 +397,8 @@ Compiler.prototype = {
351 397
       this.visit(tag.block);
352 398
 
353 399
       // pretty print
354  
-      if (this.pp && !tag.isInline() && 'pre' != tag.name && !tag.canInline()) {
  400
+      if (printIndent && 'pre' != tag.name && !tag.canInline())
355 401
         this.prettyIndent(0, true);
356  
-      }
357 402
 
358 403
       this.buffer('</' + name + '>');
359 404
     }
16  lib/lexer.js
@@ -299,7 +299,7 @@ Lexer.prototype = {
299 299
   
300 300
   block: function() {
301 301
     var captures;
302  
-    if (captures = /^block +(?:(prepend|append) +)?([^\n]+)/.exec(this.input)) {
  302
+    if (captures = /^block *(?:(prepend|append) +)?([^\n]*)/.exec(this.input)) {
303 303
       this.consume(captures[0].length);
304 304
       var mode = captures[1] || 'replace'
305 305
         , name = captures[2]
@@ -367,12 +367,20 @@ Lexer.prototype = {
367 367
    * Call mixin.
368 368
    */
369 369
   
370  
-  call: function() {
  370
+  call: function(){
371 371
     var captures;
372  
-    if (captures = /^\+([-\w]+)(?: *\((.*)\))?/.exec(this.input)) {
  372
+    if (captures = /^\+([-\w]+)/.exec(this.input)) {
373 373
       this.consume(captures[0].length);
374 374
       var tok = this.tok('call', captures[1]);
375  
-      tok.args = captures[2];
  375
+      
  376
+      // Check for args (not attributes)
  377
+      if (captures = /^ *\(([^\)\n]*)\)/.exec(this.input)) {
  378
+        if (!/^ *[-\w]+ *=/.test(captures[1])) {
  379
+          this.consume(captures[0].length);
  380
+          tok.args = captures[1];
  381
+        }
  382
+      }
  383
+      
376 384
       return tok;
377 385
     }
378 386
   },
48  lib/nodes/mixin.js
@@ -23,6 +23,7 @@ var Node = require('./node');
23 23
 var Mixin = module.exports = function Mixin(name, args, block, call){
24 24
   this.name = name;
25 25
   this.args = args;
  26
+  this.attrs = [];
26 27
   this.block = block;
27 28
   this.call = call;
28 29
 };
@@ -33,3 +34,50 @@ var Mixin = module.exports = function Mixin(name, args, block, call){
33 34
 
34 35
 Mixin.prototype.__proto__ = Node.prototype;
35 36
 
  37
+/**
  38
+ * Set attribute `name` to `val`, keep in mind these become
  39
+ * part of a raw js object literal, so to quote a value you must
  40
+ * '"quote me"', otherwise or example 'user.name' is literal JavaScript.
  41
+ *
  42
+ * @param {String} name
  43
+ * @param {String} val
  44
+ * @param {Boolean} escaped
  45
+ * @return {Tag} for chaining
  46
+ * @api public
  47
+ */
  48
+
  49
+Mixin.prototype.setAttribute = function(name, val, escaped){
  50
+  this.attrs.push({ name: name, val: val, escaped: escaped });
  51
+  return this;
  52
+};
  53
+
  54
+/**
  55
+ * Remove attribute `name` when present.
  56
+ *
  57
+ * @param {String} name
  58
+ * @api public
  59
+ */
  60
+
  61
+Mixin.prototype.removeAttribute = function(name){
  62
+  for (var i = 0, len = this.attrs.length; i < len; ++i) {
  63
+    if (this.attrs[i] && this.attrs[i].name == name) {
  64
+      delete this.attrs[i];
  65
+    }
  66
+  }
  67
+};
  68
+
  69
+/**
  70
+ * Get attribute value by `name`.
  71
+ *
  72
+ * @param {String} name
  73
+ * @return {String}
  74
+ * @api public
  75
+ */
  76
+
  77
+Mixin.prototype.getAttribute = function(name){
  78
+  for (var i = 0, len = this.attrs.length; i < len; ++i) {
  79
+    if (this.attrs[i] && this.attrs[i].name == name) {
  80
+      return this.attrs[i].val;
  81
+    }
  82
+  }
  83
+};
28  lib/parser.js
@@ -496,13 +496,12 @@ Parser.prototype = {
496 496
     var tok = this.expect('call')
497 497
       , name = tok.val
498 498
       , args = tok.args
499  
-      , mixin = this.mixins[name];
500  
-
501  
-    var block = 'indent' == this.peek().type
502  
-      ? this.block()
503  
-      : null;
504  
-
505  
-    return new nodes.Mixin(name, args, block, true);
  499
+      , mixin = new nodes.Mixin(name, args, new nodes.Block, true);
  500
+    
  501
+    this.tag(mixin);
  502
+    if (!mixin.block.nodes.length)
  503
+      mixin.block = null;
  504
+    return mixin;
506 505
   },
507 506
 
508 507
   /**
@@ -578,7 +577,7 @@ Parser.prototype = {
578 577
   },
579 578
 
580 579
   /**
581  
-   * tag (attrs | class | id)* (text | code | ':')? newline* block?
  580
+   * tag
582 581
    */
583 582
   
584 583
   parseTag: function(){
@@ -592,8 +591,17 @@ Parser.prototype = {
592 591
     }
593 592
 
594 593
     var name = this.advance().val
595  
-      , tag = new nodes.Tag(name)
596  
-      , dot;
  594
+      , tag = new nodes.Tag(name);
  595
+    
  596
+    return this.tag(tag);
  597
+  },
  598
+  
  599
+  /**
  600
+   * (attrs | class | id)* (text | code | ':')? newline* block?
  601
+   */
  602
+  
  603
+  tag: function(tag) {
  604
+    var dot;
597 605
 
598 606
     tag.line = this.line();
599 607
 
15  test/cases/mixin.blocks.html
@@ -2,8 +2,8 @@
2 2
   <body>
3 3
     <form method="GET" action="/search">
4 4
       <input type="hidden" name="_csrf" value="hey"/>
5  
-    <input type="text" name="query" placeholder="Search"/>
6  
-    <input type="submit" value="Search"/>
  5
+      <input type="text" name="query" placeholder="Search"/>
  6
+      <input type="submit" value="Search"/>
7 7
     </form>
8 8
   </body>
9 9
 </html>
@@ -11,8 +11,8 @@
11 11
   <body>
12 12
     <form method="POST" action="/search">
13 13
       <input type="hidden" name="_csrf" value="hey"/>
14  
-    <input type="text" name="query" placeholder="Search"/>
15  
-    <input type="submit" value="Search"/>
  14
+      <input type="text" name="query" placeholder="Search"/>
  15
+      <input type="submit" value="Search"/>
16 16
     </form>
17 17
   </body>
18 18
 </html>
@@ -25,7 +25,8 @@
25 25
 </html>
26 26
 <div id="foo">
27 27
   <div id="bar">
28  
-<p>one</p>
29  
-<p>two</p>
30  
-<p>three</p></div>
  28
+    <p>one</p>
  29
+    <p>two</p>
  30
+    <p>three</p>
  31
+  </div>
31 32
 </div>
9  test/cases/mixin.blocks.jade
@@ -4,7 +4,7 @@ mixin form(method, action)
4 4
   form(method=method, action=action)
5 5
     csrf_token_from_somewhere = 'hey'
6 6
     input(type='hidden', name='_csrf', value=csrf_token_from_somewhere)
7  
-    != content
  7
+    block
8 8
 
9 9
 html
10 10
   body
@@ -22,12 +22,13 @@ html
22 22
   body
23 23
     +form('POST', '/search')
24 24
 
25  
-mixin bar(html)
26  
-  #bar!= html
  25
+mixin bar()
  26
+  #bar
  27
+    block
27 28
 
28 29
 mixin foo()
29 30
   #foo
30  
-    +bar(content)
  31
+    +bar
31 32
 
32 33
 +foo
33 34
   p one
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.