Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial support for mixin attributes

Uses tag code to do its magic
  • Loading branch information...
commit b9210e8fff617b4f25a4407227f8650cb3798064 1 parent 8ec78bb
@chowey chowey authored
View
60 jade.md
@@ -434,3 +434,63 @@
<p>Amazing article</p>
</div>
</div>
+
+ Mixins can even take attributes, just like a tag. When
+ attributes are passed they become the implicit `attributes`
+ argument. Individual attributes can be accessed just like
+ normal object properties:
+
+ mixin centered
+ .centered(class=attributes.class)
+ block
+
+ +centered.bold Hello world
+
+ +centered.red
+ p This is my
+ p Amazing article
+
+ yields:
+
+ <div class="centered bold">Hello world</div>
+ <div class="centered red">
+ <p>This is my</p>
+ <p>Amazing article</p>
+ </div>
+
+ If you use `attributes` directly, *all* passed attributes
+ get used:
+
+ mixin link
+ a.menu(attributes)
+ block
+
+ +link.highlight(href='#top') Top
+ +link#sec1.plain(href='#section1') Section 1
+ +link#sec2.plain(href='#section2') Section 2
+
+ yields:
+
+ <a href="#top" class="highlight menu">Top</a>
+ <a id="sec1" href="#section1" class="plain menu">Section 1</a>
+ <a id="sec2" href="#section2" class="plain menu">Section 2</a>
+
+ If you pass arguments, they must directly follow the mixin:
+
+ mixin list(arr)
+ if block
+ .title
+ block
+ ul(attributes)
+ each item in arr
+ li= item
+
+ +list(['foo', 'bar', 'baz'])(id='myList', class='bold')
+
+ yields:
+
+ <ul id="myList" class="bold">
+ <li>foo</li>
+ <li>bar</li>
+ <li>baz</li>
+ </ul>
View
73 lib/compiler.js
@@ -286,37 +286,61 @@ Compiler.prototype = {
visitMixin: function(mixin){
var name = mixin.name.replace(/-/g, '_') + '_mixin'
, args = mixin.args || ''
+ , block = mixin.block
+ , attrs = mixin.attrs
, pp = this.pp;
if (mixin.call) {
if (pp) this.buf.push("__indent.push('" + Array(this.indents + 1).join(' ') + "');")
- if (mixin.block) {
- this.buf.push(name + '.call({block: function(){');
+ if (block || attrs.length) {
- // Render block with no indents, dynamically added when rendered
- this.parentIndents++;
- var _indents = this.indents;
- this.indents = 0;
- this.visit(mixin.block);
- this.indents = _indents;
- this.parentIndents--;
+ this.buf.push(name + '.call({');
+
+ if (block) {
+ this.buf.push('block: function(){');
+
+ // Render block with no indents, dynamically added when rendered
+ this.parentIndents++;
+ var _indents = this.indents;
+ this.indents = 0;
+ this.visit(mixin.block);
+ this.indents = _indents;
+ this.parentIndents--;
+
+ if (attrs.length) {
+ this.buf.push('},');
+ } else {
+ this.buf.push('}');
+ }
+ }
+
+ if (attrs.length) {
+ var val = this.attrs(attrs);
+ if (val.inherits) {
+ this.buf.push('attributes: merge({' + val.buf
+ + '}, attributes), escaped: merge(' + val.escaped + ', __escaped)');
+ } else {
+ this.buf.push('attributes: {' + val.buf + '}, excaped: ' + val.escaped);
+ }
+ }
if (args) {
- this.buf.push('}}, ' + args + ');');
+ this.buf.push('}, ' + args + ');');
} else {
- this.buf.push('}});');
+ this.buf.push('});');
}
+
} else {
this.buf.push(name + '(' + args + ');');
}
if (pp) this.buf.push("__indent.pop();")
} else {
this.buf.push('var ' + name + ' = function(' + args + '){');
- this.buf.push('var block = this.block;');
+ this.buf.push('var block = this.block, attributes = this.attributes || {}, __escaped = this.escaped || {};');
this.parentIndents++;
- this.visit(mixin.block);
+ this.visit(block);
this.parentIndents--;
- this.buf.push('}');
+ this.buf.push('};');
}
},
@@ -522,13 +546,25 @@ Compiler.prototype = {
*/
visitAttributes: function(attrs){
+ var val = this.attrs(attrs);
+ if (val.inherits) {
+ this.buf.push("buf.push(attrs(merge({ " + val.buf +
+ " }, attributes), merge(" + val.escaped + ", __escaped)));");
+ } else {
+ this.buf.push("buf.push(attrs({ " + val.buf + " }, " + val.escaped + "));");
+ }
+ },
+
+ attrs: function(attrs){
var buf = []
, classes = []
- , escaped = {};
+ , escaped = {}
+ , inherits = false;
if (this.terse) buf.push('terse: true');
attrs.forEach(function(attr){
+ if (attr.name == 'attributes') return inherits = true;
escaped[attr.name] = attr.escaped;
if (attr.name == 'class') {
classes.push('(' + attr.val + ')');
@@ -543,8 +579,11 @@ Compiler.prototype = {
buf.push("class: " + classes);
}
- buf = buf.join(', ').replace('class:', '"class":');
- this.buf.push("buf.push(attrs({ " + buf + " }, " + JSON.stringify(escaped) + "));");
+ return {
+ buf: buf.join(', ').replace('class:', '"class":'),
+ escaped: JSON.stringify(escaped),
+ inherits: inherits
+ };
}
};
View
6 lib/jade.js
@@ -154,15 +154,15 @@ exports.compile = function(str, options){
}
if (client) {
- fn = 'var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;\n' + fn;
+ fn = 'var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow, merge = jade.merge;\n' + fn;
}
- fn = new Function('locals, attrs, escape, rethrow', fn);
+ fn = new Function('locals, attrs, escape, rethrow, merge', fn);
if (client) return fn;
return function(locals){
- return fn(locals, runtime.attrs, runtime.escape, runtime.rethrow);
+ return fn(locals, runtime.attrs, runtime.escape, runtime.rethrow, runtime.merge);
};
};
View
14 lib/lexer.js
@@ -368,12 +368,20 @@ Lexer.prototype = {
* Call mixin.
*/
- call: function() {
+ call: function(){
var captures;
- if (captures = /^\+([-\w]+)(?: *\((.*)\))?/.exec(this.input)) {
+ if (captures = /^\+([-\w]+)/.exec(this.input)) {
this.consume(captures[0].length);
var tok = this.tok('call', captures[1]);
- tok.args = captures[2];
+
+ // Check for args (not attributes)
+ if (captures = /^ *\(([^\)\n]*)\)/.exec(this.input)) {
+ if (!/^ *[-\w]+ *=/.test(captures[1])) {
+ this.consume(captures[0].length);
+ tok.args = captures[1];
+ }
+ }
+
return tok;
}
},
View
77 lib/nodes/attrs.js
@@ -0,0 +1,77 @@
+
+/*!
+ * Jade - nodes - Attrs
+ * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+
+var Node = require('./node'),
+ Block = require('./block');
+
+/**
+ * Initialize a `Attrs` node.
+ *
+ * @api public
+ */
+
+var Attrs = module.exports = function Attrs() {
+ this.attrs = [];
+};
+
+/**
+ * Inherit from `Node`.
+ */
+
+Attrs.prototype.__proto__ = Node.prototype;
+
+/**
+ * Set attribute `name` to `val`, keep in mind these become
+ * part of a raw js object literal, so to quote a value you must
+ * '"quote me"', otherwise or example 'user.name' is literal JavaScript.
+ *
+ * @param {String} name
+ * @param {String} val
+ * @param {Boolean} escaped
+ * @return {Tag} for chaining
+ * @api public
+ */
+
+Attrs.prototype.setAttribute = function(name, val, escaped){
+ this.attrs.push({ name: name, val: val, escaped: escaped });
+ return this;
+};
+
+/**
+ * Remove attribute `name` when present.
+ *
+ * @param {String} name
+ * @api public
+ */
+
+Attrs.prototype.removeAttribute = function(name){
+ for (var i = 0, len = this.attrs.length; i < len; ++i) {
+ if (this.attrs[i] && this.attrs[i].name == name) {
+ delete this.attrs[i];
+ }
+ }
+};
+
+/**
+ * Get attribute value by `name`.
+ *
+ * @param {String} name
+ * @return {String}
+ * @api public
+ */
+
+Attrs.prototype.getAttribute = function(name){
+ for (var i = 0, len = this.attrs.length; i < len; ++i) {
+ if (this.attrs[i] && this.attrs[i].name == name) {
+ return this.attrs[i].val;
+ }
+ }
+};
View
7 lib/nodes/mixin.js
@@ -9,7 +9,7 @@
* Module dependencies.
*/
-var Node = require('./node');
+var Attrs = require('./attrs');
/**
* Initialize a new `Mixin` with `name` and `block`.
@@ -24,12 +24,13 @@ var Mixin = module.exports = function Mixin(name, args, block, call){
this.name = name;
this.args = args;
this.block = block;
+ this.attrs = [];
this.call = call;
};
/**
- * Inherit from `Node`.
+ * Inherit from `Attrs`.
*/
-Mixin.prototype.__proto__ = Node.prototype;
+Mixin.prototype.__proto__ = Attrs.prototype;
View
54 lib/nodes/tag.js
@@ -9,7 +9,7 @@
* Module dependencies.
*/
-var Node = require('./node'),
+var Attrs = require('./attrs'),
Block = require('./block'),
inlineTags = require('../inline-tags');
@@ -28,10 +28,10 @@ var Tag = module.exports = function Tag(name, block) {
};
/**
- * Inherit from `Node`.
+ * Inherit from `Attrs`.
*/
-Tag.prototype.__proto__ = Node.prototype;
+Tag.prototype.__proto__ = Attrs.prototype;
/**
* Clone this tag.
@@ -49,54 +49,6 @@ Tag.prototype.clone = function(){
};
/**
- * Set attribute `name` to `val`, keep in mind these become
- * part of a raw js object literal, so to quote a value you must
- * '"quote me"', otherwise or example 'user.name' is literal JavaScript.
- *
- * @param {String} name
- * @param {String} val
- * @param {Boolean} escaped
- * @return {Tag} for chaining
- * @api public
- */
-
-Tag.prototype.setAttribute = function(name, val, escaped){
- this.attrs.push({ name: name, val: val, escaped: escaped });
- return this;
-};
-
-/**
- * Remove attribute `name` when present.
- *
- * @param {String} name
- * @api public
- */
-
-Tag.prototype.removeAttribute = function(name){
- for (var i = 0, len = this.attrs.length; i < len; ++i) {
- if (this.attrs[i] && this.attrs[i].name == name) {
- delete this.attrs[i];
- }
- }
-};
-
-/**
- * Get attribute value by `name`.
- *
- * @param {String} name
- * @return {String}
- * @api public
- */
-
-Tag.prototype.getAttribute = function(name){
- for (var i = 0, len = this.attrs.length; i < len; ++i) {
- if (this.attrs[i] && this.attrs[i].name == name) {
- return this.attrs[i].val;
- }
- }
-};
-
-/**
* Check if this tag is an inline tag.
*
* @return {Boolean}
View
20 lib/parser.js
@@ -496,13 +496,12 @@ Parser.prototype = {
var tok = this.expect('call')
, name = tok.val
, args = tok.args
- , mixin = this.mixins[name];
+ , mixin = new nodes.Mixin(name, args, new nodes.Block, true);
- var block = 'indent' == this.peek().type
- ? this.block()
- : null;
-
- return new nodes.Mixin(name, args, block, true);
+ this.tag(mixin);
+ if (!mixin.block.nodes.length)
+ mixin.block = null;
+ return mixin;
},
/**
@@ -592,8 +591,13 @@ Parser.prototype = {
}
var name = this.advance().val
- , tag = new nodes.Tag(name)
- , dot;
+ , tag = new nodes.Tag(name);
+
+ return this.tag(tag);
+ },
+
+ tag: function(tag){
+ var dot;
tag.line = this.line();
View
24 lib/runtime.js
@@ -32,6 +32,30 @@ if (!Object.keys) {
}
/**
+* Merge two attribute objects. `b` attributes override `a` attributes.
+* For "class" attributes, `b` attributes append to `a` attributes.
+*
+* @param {Object} `a` object
+* @param {Object} `b` object
+* @param {Boolean} `true` if these objects are `escaped` objects, not attributes
+* @api private
+*/
+
+exports.merge = function merge(a, b, escaped) {
+ // Special treatment for "class" attribute
+ if (!escaped) {
+ var val;
+ (val = a['class']) && Array.isArray(val) && (a['class'] = val.join(' '));
+ (val = b['class']) && Array.isArray(val) && (b['class'] = val.join(' '));
+ a['class'] && b['class'] && (b['class'] += ' ' + a['class']);
+ }
+
+ for (var key in b)
+ a[key] = b[key];
+ return a;
+};
+
+/**
* Render the given attributes object.
*
* @param {Object} obj
View
23 test/cases/mixin.attrs.html
@@ -0,0 +1,23 @@
+<body>
+ <div id="First" class="centered">Hello World
+ </div>
+ <div id="Second" class="centered">
+ <h1>Section 1</h1>
+ <p>Some important content.</p>
+ </div>
+ <div id="Third" class="centered">
+ <h1 class="foo bar">Section 2</h1>
+ <p>Even more important content.</p>
+ <div class="footer"><a href="menu.html">Back</a></div>
+ </div>
+ <div class="stretch">
+ <div class="centered">
+ <h1 class="highlight">Section 3</h1>
+ <p>Last content.</p>
+ <div class="footer"><a href="#">Back</a></div>
+ </div>
+ </div>
+ <div class="foo bar bottom" name="end" id="Last" data-attr="baz">
+ <p>Some final words.</p>
+ </div>
+</body>
View
28 test/cases/mixin.attrs.jade
@@ -0,0 +1,28 @@
+mixin centered(title)
+ div.centered(id=attributes.id)
+ - if (title)
+ h1(class=attributes.class)= title
+ block
+ - if (attributes.href)
+ .footer
+ a(href=attributes.href) Back
+
+mixin main(title)
+ div.stretch
+ +centered(title).highlight(attributes)
+ block
+
+mixin bottom
+ div.bottom(attributes)
+ block
+
+body
+ +centered#First Hello World
+ +centered('Section 1')#Second
+ p Some important content.
+ +centered('Section 2')#Third.foo(href='menu.html', class='bar')
+ p Even more important content.
+ +main('Section 3')(href='#')
+ p Last content.
+ +bottom.foo(class='bar', name='end', id='Last', data-attr='baz')
+ p Some final words.
Please sign in to comment.
Something went wrong with that request. Please try again.