jader 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore CHANGED
@@ -6,3 +6,5 @@ pkg/*
6
6
  .rvmrc
7
7
  *.sqlite
8
8
  spec/dummy/tmp/
9
+ doc/
10
+ .yardoc
data/README.md CHANGED
@@ -43,7 +43,7 @@ The most significant differences between using standard server-side Ruby-based e
43
43
 
44
44
  Our template code should look like this:
45
45
 
46
- ```
46
+ ```jade
47
47
  ul.users
48
48
  each user in users
49
49
  li.user= user.name
@@ -58,7 +58,7 @@ Since Rails doesn't expect server-side templates to live under `app/assets` we n
58
58
 
59
59
  Inside your `application_controller.rb` add the following:
60
60
 
61
- ```
61
+ ```ruby
62
62
  class ApplicationController < ActionController::Base
63
63
  protect_from_forgery
64
64
 
@@ -131,7 +131,7 @@ Jader does not assume your Rails models should be serialized by default. Instead
131
131
 
132
132
  To enable this behaviour, consider the following example:
133
133
 
134
- ```
134
+ ```ruby
135
135
  class User < ActiveRecord::Base
136
136
 
137
137
  include Jader::Serialize
@@ -149,7 +149,7 @@ By default, calling `jade_serializable` with no arguments will serialize all you
149
149
 
150
150
  Consider the following code:
151
151
 
152
- ```
152
+ ```ruby
153
153
  # define our model
154
154
  class User < ActiveRecord::Base
155
155
 
@@ -180,7 +180,7 @@ which attributes should always be serialized, and whether we'd like these attrib
180
180
  Consider the following code:
181
181
 
182
182
 
183
- ```
183
+ ```ruby
184
184
  # define our models
185
185
 
186
186
  class Favorite < ActiveRecord::Base
@@ -221,7 +221,7 @@ To only serialize the specified attributes, call `jade_serializable` with `:merg
221
221
 
222
222
  Invokation format for `jade_serializable` is:
223
223
 
224
- ```
224
+ ```ruby
225
225
  jade_serializable :attr1, :attr2, :attr3 ...., :merge => true/false
226
226
  ```
227
227
 
@@ -284,7 +284,7 @@ For the sake of this example, we assume `gem 'i18n-js'` is installed in our appl
284
284
 
285
285
  Our `application.js` file will then include:
286
286
 
287
- ```
287
+ ``` javascript
288
288
  //= require i18n
289
289
  //= require i18n/translations
290
290
  ```
@@ -332,7 +332,7 @@ Jader is built upon the wonderful work of [Boris Staal](https://github.com/round
332
332
  - [ruby-haml-js](https://github.com/dnagir/ruby-haml-js)
333
333
  - [tilt-jade](https://github.com/therabidbanana/tilt-jade)
334
334
 
335
- Boris Staal's Jade Rubygem It was developed as a successor to tilt-jade to improve following:
335
+ Boris Staal's [Jade](https://github.com/roundlake/jade/) Rubygem was developed as a successor to tilt-jade to improve following:
336
336
 
337
337
  * Add debugging capabilities (slightly different build technique)
338
338
  * Support exact Jade.JS lib without modifications
data/jader.gemspec CHANGED
@@ -18,11 +18,12 @@ Gem::Specification.new do |s|
18
18
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
19
  s.require_paths = ["lib"]
20
20
 
21
- s.add_dependency 'execjs'
21
+ s.add_dependency 'libv8'
22
22
  s.add_dependency 'tilt'
23
23
  s.add_dependency 'sprockets'
24
24
  s.add_development_dependency 'rspec'
25
25
  s.add_development_dependency 'rspec-rails'
26
26
  s.add_development_dependency 'rails', '>= 3.1'
27
27
  s.add_development_dependency 'pry'
28
+ s.add_development_dependency 'yard'
28
29
  end
@@ -1,9 +1,13 @@
1
- require 'execjs'
1
+ require 'v8'
2
2
 
3
3
  module Jader
4
4
  class Compiler
5
5
  attr_accessor :options
6
6
 
7
+ # Initialize Jader template compiler. @see https://github.com/visionmedia/jade#options
8
+ # @param [Hash] options Jade compiler options
9
+ # @option options [Boolean] :client run Jade compiler in client / server mode (default `true`)
10
+ # @option options [Boolean] :compileDebug run Jade compiler in debug mode(default `false`)
7
11
  def initialize(options={})
8
12
  @options = {
9
13
  :client => true,
@@ -11,6 +15,8 @@ module Jader
11
15
  }.merge options
12
16
  end
13
17
 
18
+ # Jade template engine Javascript source code used to compile templates in ExecJS
19
+ # @return [String] Jade source code
14
20
  def source
15
21
  @source ||= %{
16
22
  var window = {};
@@ -19,40 +25,63 @@ module Jader
19
25
  }
20
26
  end
21
27
 
22
- def context
23
- @context ||= ExecJS.compile source
28
+ # V8 context with Jade code compiled
29
+ # @yield [context] V8::Context compiled Jade source code in V8 context
30
+ def v8_context
31
+ V8::C::Locker() do
32
+ context = V8::Context.new
33
+ context.eval(source)
34
+ yield context
35
+ end
24
36
  end
25
37
 
38
+ # Jade Javascript engine version
39
+ # @return [String] version of Jade javascript engine installed in `vendor/assets/javascripts`
26
40
  def jade_version
27
- context.eval("jade.version")
41
+ v8_context do |context|
42
+ context.eval("jade.version")
43
+ end
28
44
  end
29
45
 
46
+ # Compile a Jade template for client-side use with JST
47
+ # @param [String, File] template Jade template file or text to compile
48
+ # @param [String] file_name name of template file used to resolve mixins inclusion
49
+ # @return [String] Jade template compiled into Javascript and wrapped inside an anonymous function for JST
30
50
  def compile(template, file_name = '')
31
- template = template.read if template.respond_to?(:read)
32
- file_name.match(/views\/([^\/]+)\//)
33
- controller_name = $1 || nil
34
- combo = (template_mixins(controller_name) << template).join("\n").to_json
35
- tmpl = context.eval("jade.precompile(#{combo}, #{@options.to_json})")
36
-
37
- %{
38
- function(locals){
39
- #{tmpl}
40
- }
41
- }
51
+ v8_context do |context|
52
+ template = template.read if template.respond_to?(:read)
53
+ file_name.match(/views\/([^\/]+)\//)
54
+ controller_name = $1 || nil
55
+ combo = (template_mixins(controller_name) << template).join("\n").to_json
56
+ context.eval("jade.compile(#{combo},#{@options.to_json})").to_s.sub('function anonymous','function')
57
+ end
42
58
  end
43
59
 
60
+ # Compile and evaluate a Jade template for server-side rendering
61
+ # @param [String] template Jade template text to render
62
+ # @param [String] controller_name name of Rails controller that's rendering the template
63
+ # @param [Hash] vars controller instance variables passed to the template
64
+ # @return [String] HTML output of compiled Jade template
44
65
  def render(template, controller_name, vars = {})
45
- combo = (template_mixins(controller_name) << template).join("\n").to_json
46
- tmpl = context.eval("jade.precompile(#{combo}, #{@options.to_json})")
47
- context.eval(%{
48
- function(locals){
49
- #{Jader::Source::runtime}
50
- #{Jader.configuration.includes.join("\n")}
51
- #{tmpl}
52
- }.call(null,#{vars.to_jade.to_json})
53
- })
66
+ v8_context do |context|
67
+ context.eval(Jader.configuration.includes.join("\n"))
68
+ combo = (template_mixins(controller_name) << template).join("\n").to_json
69
+ context.eval("var fn = jade.compile(#{combo})")
70
+ context.eval("fn(#{vars.to_jade.to_json})")
71
+ end
72
+ #tmpl = context.eval("jade.precompile(#{combo}, #{@options.to_json})")
73
+ #context.eval(%{
74
+ # function(locals){
75
+ # #{Jader::Source::runtime}
76
+ # #{Jader.configuration.includes.join("\n")}
77
+ # #{tmpl}
78
+ # }.call(null,#{vars.to_jade.to_json})
79
+ #})
54
80
  end
55
81
 
82
+ # Jade template mixins for a given controller
83
+ # @param [String] controller_name name of Rails controller rendering a Jade template
84
+ # @return [Array<String>] array of Jade mixins to use with a Jade template rendered by a Rails controller
56
85
  def template_mixins(controller_name)
57
86
  mixins = []
58
87
  unless Jader.configuration.mixins_path.nil?
@@ -3,14 +3,23 @@ module Jader
3
3
  attr_accessor :configuration
4
4
  end
5
5
 
6
+ # Configure Jader
7
+ # @yield [config] Jader::Configuration instance
8
+ # @example
9
+ # Jader.configure do |config|
10
+ # config.mixins_path = Rails.root.join('app','assets','javascripts','helpers')
11
+ # config.includes << IO.read Rails.root.join('app','assets','javascripts','util.js')
12
+ # end
6
13
  def self.configure
7
14
  self.configuration ||= Configuration.new
8
15
  yield(configuration)
9
16
  end
10
17
 
18
+ # Jader configuration class
11
19
  class Configuration
12
20
  attr_accessor :mixins_path, :includes
13
21
 
22
+ # Initialize Jader::Configuration class with default values
14
23
  def initialize
15
24
  @mixins_path = nil
16
25
  @includes = []
@@ -1,11 +1,22 @@
1
1
  module Jader
2
+ # Server side Jade templates renderer
2
3
  module Renderer
3
4
 
5
+ # Convert Jade template to HTML output for rendering as a Rails view
6
+ # @param [String] template_text Jade template text to convert
7
+ # @param [String] controller_name name of Rails controller rendering the view
8
+ # @param [Hash] vars controller instance variables passed to the template
9
+ # @return [String] HTML output of evaluated template
10
+ # @see Jader::Compiler#render
4
11
  def self.convert_template(template_text, controller_name, vars = {})
5
12
  compiler = Jader::Compiler.new :client => true
6
13
  compiler.render(template_text, controller_name, vars)
7
14
  end
8
15
 
16
+ # Prepare controller instance variables for the template and execute template conversion.
17
+ # Called as an ActionView::Template registered template
18
+ # @param [ActionView::Template] template currently rendered ActionView::Template instance
19
+ # @see Jader::Renderer#convert_template
9
20
  def self.call(template)
10
21
  template.source.gsub!(/\#\{([^\}]+)\}/,"\\\#{\\1}") # escape Jade's #{somevariable} syntax
11
22
  %{
@@ -4,6 +4,15 @@ module Jader
4
4
 
5
5
  module ClassMethods
6
6
 
7
+ # Enable serialization on ActiveModel classes
8
+ # @param [Array<Symbol>] args model attribute names to serialize
9
+ # @param [Hash] args options serializing mode
10
+ # @option args [Boolean] :merge should serialized attributes be merged with `self.attributes`
11
+ # @example
12
+ # class User < ActiveRecord::Base
13
+ # include Jader::Serialize
14
+ # jade_serializable :name, :email, :merge => false
15
+ # end
7
16
  def jade_serializable(*args)
8
17
  serialize = {
9
18
  :attrs => [],
@@ -21,10 +30,23 @@ module Jader
21
30
 
22
31
  end
23
32
 
33
+ #nodoc
24
34
  def self.included(base)
25
35
  base.extend ClassMethods
26
36
  end
27
37
 
38
+ # Serialize instance attributes to a Hash based on serializable attributes defined on Model class.
39
+ # @return [Hash] hash of model instance attributes
40
+ def to_jade
41
+ h = {:model => self.class.name.downcase}
42
+ self.jade_attributes.each do |attr|
43
+ h[attr] = self.send(attr)
44
+ end
45
+ h
46
+ end
47
+
48
+ # List of Model attributes that should be serialized when called `to_jade` on Model instance
49
+ # @return [Array] list of serializable attributes
28
50
  def jade_attributes
29
51
  s = self.class.class_variable_get(:@@serialize)
30
52
  if s[:merge]
@@ -34,20 +56,12 @@ module Jader
34
56
  end
35
57
  attrs.collect{|attr| attr.to_sym}.uniq
36
58
  end
37
-
38
- def to_jade
39
- h = {:model => self.class.name.downcase}
40
- self.jade_attributes.each do |attr|
41
- h[attr] = self.send(attr)
42
- end
43
- h
44
- end
45
-
46
59
  end
47
60
 
48
61
  end
49
62
 
50
63
  class Object
64
+ # Serialize Object to Jade format. Invoke `self.to_jade` if instance responds to `to_jade`
51
65
  def to_ice
52
66
  if self.respond_to? :to_a
53
67
  self.to_a.to_jade
@@ -57,6 +71,7 @@ class Object
57
71
  end
58
72
  end
59
73
 
74
+ #nodoc
60
75
  [FalseClass, TrueClass, Numeric, String].each do |cls|
61
76
  cls.class_eval do
62
77
  def to_jade
@@ -66,12 +81,16 @@ end
66
81
  end
67
82
 
68
83
  class Array
84
+
85
+ # Serialize Array to Jade format. Invoke `to_jade` on array members
69
86
  def to_jade
70
87
  map {|a| a.respond_to?(:to_jade) ? a.to_jade : a }
71
88
  end
72
89
  end
73
90
 
74
91
  class Hash
92
+
93
+ # Serialize Hash to Jade format. Invoke `to_jade` on members
75
94
  def to_jade
76
95
  res = {}
77
96
  each_pair do |key, value|
data/lib/jader/source.rb CHANGED
@@ -1,17 +1,23 @@
1
1
  module Jader
2
+ # Jade template engine Javascript source code
2
3
  module Source
4
+
5
+ # Jade source code
3
6
  def self.jade
4
7
  IO.read jade_path
5
8
  end
6
9
 
10
+ # Jade runtime source code
7
11
  def self.runtime
8
12
  IO.read runtime_path
9
13
  end
10
14
 
15
+ # Jade source code path
11
16
  def self.jade_path
12
17
  File.expand_path("../../../vendor/assets/javascripts/jade/jade.js", __FILE__)
13
18
  end
14
19
 
20
+ # Jade runtime source code path
15
21
  def self.runtime_path
16
22
  File.expand_path("../../../vendor/assets/javascripts/jade/runtime.js", __FILE__)
17
23
  end
@@ -1,27 +1,26 @@
1
1
  require 'tilt/template'
2
2
 
3
3
  module Jader
4
+ # Jader Tilt template for use with JST
4
5
  class Template < Tilt::Template
5
6
  self.default_mime_type = 'application/javascript'
6
7
 
8
+ # Ensure V8 is available when engine is initialized
7
9
  def self.engine_initialized?
8
- defined? ::ExecJS
10
+ defined? ::V8
9
11
  end
10
12
 
13
+ # Require 'execjs' when initializing engine
11
14
  def initialize_engine
12
- require_template_library 'execjs'
15
+ require_template_library 'v8'
13
16
  end
14
17
 
15
18
  def prepare
16
19
  end
17
20
 
21
+ # Evaluate the template. Compiles the template for JST
22
+ # @return [String] JST-compliant compiled version of the Jade template being rendered
18
23
  def evaluate(scope, locals, &block)
19
- compile_function
20
- end
21
-
22
- private
23
-
24
- def compile_function
25
24
  Jader::Compiler.new(:filename => file).compile(data, file)
26
25
  end
27
26
 
data/lib/jader/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Jader
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
@@ -12,12 +12,14 @@ describe Jader::Compiler do
12
12
  }
13
13
  end
14
14
 
15
- it "should contain ExecJS context" do
16
- @compiler.context.eval("window.jade").should_not be_empty
15
+ it "should contain v8 context" do
16
+ @compiler.v8_context do |context|
17
+ context.eval("typeof window.jade").should == 'object'
18
+ end
17
19
  end
18
20
 
19
21
  it "should define Jade.JS compiler version" do
20
- @compiler.jade_version.should == "0.20.0"
22
+ @compiler.jade_version.should == "0.27.2"
21
23
  end
22
24
 
23
25
  it "should compile small thing" do
@@ -21,16 +21,18 @@ describe Jader::Compiler do
21
21
  end
22
22
 
23
23
  it 'should work fine with JST' do
24
- context = ExecJS.compile %{
24
+ context = V8::Context.new
25
+ context.eval %{
25
26
  #{asset_for('application.js').to_s}
26
27
  html = JST['sample']({name: 'Yorik'})
27
28
  }
28
- context.eval('html').should == "<!DOCTYPE html><head><title>Hello, Yorik :)</title></head><body>Yap, it works\n</body>"
29
+ context.eval('html').should == "<!DOCTYPE html><head><title>Hello, Yorik :)</title></head><body>Yap, it works</body>"
29
30
  end
30
31
 
31
32
  it 'should use mixins in JST' do
32
33
  phrase = 'Hi There'
33
- context = ExecJS.compile %{
34
+ context = V8::Context.new
35
+ context.eval %{
34
36
  #{asset_for('application.js').to_s}
35
37
  html = JST['views/users/index']({phrase: '#{phrase}'})
36
38
  }
@@ -30,7 +30,7 @@ require.register = function (path, fn){
30
30
 
31
31
  require.relative = function (parent) {
32
32
  return function(p){
33
- if ('.' != p[0]) return require(p);
33
+ if ('.' != p.charAt(0)) return require(p);
34
34
 
35
35
  var path = parent.split('/')
36
36
  , segs = p.split('/');
@@ -63,10 +63,10 @@ var nodes = require('./nodes')
63
63
  , filters = require('./filters')
64
64
  , doctypes = require('./doctypes')
65
65
  , selfClosing = require('./self-closing')
66
- , inlineTags = require('./inline-tags')
66
+ , runtime = require('./runtime')
67
67
  , utils = require('./utils');
68
68
 
69
-
69
+
70
70
  if (!Object.keys) {
71
71
  Object.keys = function(obj){
72
72
  var arr = [];
@@ -76,9 +76,9 @@ var nodes = require('./nodes')
76
76
  }
77
77
  }
78
78
  return arr;
79
- }
79
+ }
80
80
  }
81
-
81
+
82
82
  if (!String.prototype.trimLeft) {
83
83
  String.prototype.trimLeft = function(){
84
84
  return this.replace(/^\s+/, '');
@@ -103,6 +103,7 @@ var Compiler = module.exports = function Compiler(node, options) {
103
103
  this.pp = options.pretty || false;
104
104
  this.debug = false !== options.compileDebug;
105
105
  this.indents = 0;
106
+ this.parentIndents = 0;
106
107
  if (options.doctype) this.setDoctype(options.doctype);
107
108
  };
108
109
 
@@ -111,16 +112,17 @@ var Compiler = module.exports = function Compiler(node, options) {
111
112
  */
112
113
 
113
114
  Compiler.prototype = {
114
-
115
+
115
116
  /**
116
117
  * Compile parse tree to JavaScript.
117
118
  *
118
119
  * @api public
119
120
  */
120
-
121
+
121
122
  compile: function(){
122
123
  this.buf = ['var interp;'];
123
- this.lastBufferedIdx = -1
124
+ if (this.pp) this.buf.push("var __indent = [];");
125
+ this.lastBufferedIdx = -1;
124
126
  this.visit(this.node);
125
127
  return this.buf.join('\n');
126
128
  },
@@ -133,15 +135,14 @@ Compiler.prototype = {
133
135
  * @param {string} name
134
136
  * @api public
135
137
  */
136
-
138
+
137
139
  setDoctype: function(name){
138
- var doctype = doctypes[(name || 'default').toLowerCase()];
139
- doctype = doctype || '<!DOCTYPE ' + name + '>';
140
- this.doctype = doctype;
141
- this.terse = '5' == name || 'html' == name;
140
+ name = (name && name.toLowerCase()) || 'default';
141
+ this.doctype = doctypes[name] || '<!DOCTYPE ' + name + '>';
142
+ this.terse = this.doctype.toLowerCase() == '<!doctype html>';
142
143
  this.xml = 0 == this.doctype.indexOf('<?xml');
143
144
  },
144
-
145
+
145
146
  /**
146
147
  * Buffer the given `str` optionally escaped.
147
148
  *
@@ -149,10 +150,10 @@ Compiler.prototype = {
149
150
  * @param {Boolean} esc
150
151
  * @api public
151
152
  */
152
-
153
+
153
154
  buffer: function(str, esc){
154
155
  if (esc) str = utils.escape(str);
155
-
156
+
156
157
  if (this.lastBufferedIdx == this.buf.length) {
157
158
  this.lastBuffered += str;
158
159
  this.buf[this.lastBufferedIdx - 1] = "buf.push('" + this.lastBuffered + "');"
@@ -160,23 +161,40 @@ Compiler.prototype = {
160
161
  this.buf.push("buf.push('" + str + "');");
161
162
  this.lastBuffered = str;
162
163
  this.lastBufferedIdx = this.buf.length;
163
- }
164
+ }
164
165
  },
165
-
166
+
167
+ /**
168
+ * Buffer an indent based on the current `indent`
169
+ * property and an additional `offset`.
170
+ *
171
+ * @param {Number} offset
172
+ * @param {Boolean} newline
173
+ * @api public
174
+ */
175
+
176
+ prettyIndent: function(offset, newline){
177
+ offset = offset || 0;
178
+ newline = newline ? '\\n' : '';
179
+ this.buffer(newline + Array(this.indents + offset).join(' '));
180
+ if (this.parentIndents)
181
+ this.buf.push("buf.push.apply(buf, __indent);");
182
+ },
183
+
166
184
  /**
167
185
  * Visit `node`.
168
186
  *
169
187
  * @param {Node} node
170
188
  * @api public
171
189
  */
172
-
190
+
173
191
  visit: function(node){
174
192
  var debug = this.debug;
175
193
 
176
194
  if (debug) {
177
195
  this.buf.push('__jade.unshift({ lineno: ' + node.line
178
196
  + ', filename: ' + (node.filename
179
- ? '"' + node.filename + '"'
197
+ ? JSON.stringify(node.filename)
180
198
  : '__jade[0].filename')
181
199
  + ' });');
182
200
  }
@@ -192,14 +210,14 @@ Compiler.prototype = {
192
210
 
193
211
  if (debug) this.buf.push('__jade.shift();');
194
212
  },
195
-
213
+
196
214
  /**
197
215
  * Visit `node`.
198
216
  *
199
217
  * @param {Node} node
200
218
  * @api public
201
219
  */
202
-
220
+
203
221
  visitNode: function(node){
204
222
  var name = node.constructor.name
205
223
  || node.constructor.toString().match(/function ([^(\s]+)()/)[1];
@@ -221,7 +239,7 @@ Compiler.prototype = {
221
239
  this.buf.push('}');
222
240
  this.withinCase = _;
223
241
  },
224
-
242
+
225
243
  /**
226
244
  * Visit when `node`.
227
245
  *
@@ -259,12 +277,34 @@ Compiler.prototype = {
259
277
  */
260
278
 
261
279
  visitBlock: function(block){
262
- var len = block.nodes.length;
280
+ var len = block.nodes.length
281
+ , escape = this.escape
282
+ , pp = this.pp
283
+
284
+ // Block keyword has a special meaning in mixins
285
+ if (this.parentIndents && block.mode) {
286
+ if (pp) this.buf.push("__indent.push('" + Array(this.indents + 1).join(' ') + "');")
287
+ this.buf.push('block && block();');
288
+ if (pp) this.buf.push("__indent.pop();")
289
+ return;
290
+ }
291
+
292
+ // Pretty print multi-line text
293
+ if (pp && len > 1 && !escape && block.nodes[0].isText && block.nodes[1].isText)
294
+ this.prettyIndent(1, true);
295
+
263
296
  for (var i = 0; i < len; ++i) {
297
+ // Pretty print text
298
+ if (pp && i > 0 && !escape && block.nodes[i].isText && block.nodes[i-1].isText)
299
+ this.prettyIndent(1, false);
300
+
264
301
  this.visit(block.nodes[i]);
302
+ // Multiple text nodes are separated by newlines
303
+ if (block.nodes[i+1] && block.nodes[i].isText && block.nodes[i+1].isText)
304
+ this.buffer('\\n');
265
305
  }
266
306
  },
267
-
307
+
268
308
  /**
269
309
  * Visit `doctype`. Sets terse mode to `true` when html 5
270
310
  * is used, causing self-closing tags to end with ">" vs "/>",
@@ -273,7 +313,7 @@ Compiler.prototype = {
273
313
  * @param {Doctype} doctype
274
314
  * @api public
275
315
  */
276
-
316
+
277
317
  visitDoctype: function(doctype){
278
318
  if (doctype && (doctype.val || !this.doctype)) {
279
319
  this.setDoctype(doctype.val || 'default');
@@ -293,14 +333,62 @@ Compiler.prototype = {
293
333
 
294
334
  visitMixin: function(mixin){
295
335
  var name = mixin.name.replace(/-/g, '_') + '_mixin'
296
- , args = mixin.args || '';
336
+ , args = mixin.args || ''
337
+ , block = mixin.block
338
+ , attrs = mixin.attrs
339
+ , pp = this.pp;
340
+
341
+ if (mixin.call) {
342
+ if (pp) this.buf.push("__indent.push('" + Array(this.indents + 1).join(' ') + "');")
343
+ if (block || attrs.length) {
344
+
345
+ this.buf.push(name + '.call({');
346
+
347
+ if (block) {
348
+ this.buf.push('block: function(){');
349
+
350
+ // Render block with no indents, dynamically added when rendered
351
+ this.parentIndents++;
352
+ var _indents = this.indents;
353
+ this.indents = 0;
354
+ this.visit(mixin.block);
355
+ this.indents = _indents;
356
+ this.parentIndents--;
357
+
358
+ if (attrs.length) {
359
+ this.buf.push('},');
360
+ } else {
361
+ this.buf.push('}');
362
+ }
363
+ }
297
364
 
298
- if (mixin.block) {
299
- this.buf.push('var ' + name + ' = function(' + args + '){');
300
- this.visit(mixin.block);
301
- this.buf.push('}');
365
+ if (attrs.length) {
366
+ var val = this.attrs(attrs);
367
+ if (val.inherits) {
368
+ this.buf.push('attributes: merge({' + val.buf
369
+ + '}, attributes), escaped: merge(' + val.escaped + ', escaped, true)');
370
+ } else {
371
+ this.buf.push('attributes: {' + val.buf + '}, escaped: ' + val.escaped);
372
+ }
373
+ }
374
+
375
+ if (args) {
376
+ this.buf.push('}, ' + args + ');');
377
+ } else {
378
+ this.buf.push('});');
379
+ }
380
+
381
+ } else {
382
+ this.buf.push(name + '(' + args + ');');
383
+ }
384
+ if (pp) this.buf.push("__indent.pop();")
302
385
  } else {
303
- this.buf.push(name + '(' + args + ');');
386
+ this.buf.push('var ' + name + ' = function(' + args + '){');
387
+ this.buf.push('var block = this.block, attributes = this.attributes || {}, escaped = this.escaped || {};');
388
+ this.parentIndents++;
389
+ this.visit(block);
390
+ this.parentIndents--;
391
+ this.buf.push('};');
304
392
  }
305
393
  },
306
394
 
@@ -311,10 +399,13 @@ Compiler.prototype = {
311
399
  * @param {Tag} tag
312
400
  * @api public
313
401
  */
314
-
402
+
315
403
  visitTag: function(tag){
316
404
  this.indents++;
317
- var name = tag.name;
405
+ var name = tag.name
406
+ , pp = this.pp;
407
+
408
+ if (tag.buffer) name = "' + (" + name + ") + '";
318
409
 
319
410
  if (!this.hasCompiledTag) {
320
411
  if (!this.hasCompiledDoctype && 'html' == name) {
@@ -324,11 +415,10 @@ Compiler.prototype = {
324
415
  }
325
416
 
326
417
  // pretty print
327
- if (this.pp && inlineTags.indexOf(name) == -1) {
328
- this.buffer('\\n' + Array(this.indents).join(' '));
329
- }
418
+ if (pp && !tag.isInline())
419
+ this.prettyIndent(0, true);
330
420
 
331
- if (~selfClosing.indexOf(name) && !this.xml) {
421
+ if ((~selfClosing.indexOf(name) || tag.selfClosing) && !this.xml) {
332
422
  this.buffer('<' + name);
333
423
  this.visitAttributes(tag.attrs);
334
424
  this.terse
@@ -344,27 +434,25 @@ Compiler.prototype = {
344
434
  this.buffer('<' + name + '>');
345
435
  }
346
436
  if (tag.code) this.visitCode(tag.code);
347
- if (tag.text) this.buffer(utils.text(tag.text.nodes[0].trimLeft()));
348
437
  this.escape = 'pre' == tag.name;
349
438
  this.visit(tag.block);
350
439
 
351
440
  // pretty print
352
- if (this.pp && !~inlineTags.indexOf(name) && !tag.textOnly) {
353
- this.buffer('\\n' + Array(this.indents).join(' '));
354
- }
441
+ if (pp && !tag.isInline() && 'pre' != tag.name && !tag.canInline())
442
+ this.prettyIndent(0, true);
355
443
 
356
444
  this.buffer('</' + name + '>');
357
445
  }
358
446
  this.indents--;
359
447
  },
360
-
448
+
361
449
  /**
362
450
  * Visit `filter`, throwing when the filter does not exist.
363
451
  *
364
452
  * @param {Filter} filter
365
453
  * @api public
366
454
  */
367
-
455
+
368
456
  visitFilter: function(filter){
369
457
  var fn = filters[filter.name];
370
458
 
@@ -376,48 +464,50 @@ Compiler.prototype = {
376
464
  throw new Error('unknown filter ":' + filter.name + '"');
377
465
  }
378
466
  }
467
+
379
468
  if (filter.isASTFilter) {
380
469
  this.buf.push(fn(filter.block, this, filter.attrs));
381
470
  } else {
382
- var text = filter.block.nodes.join('');
471
+ var text = filter.block.nodes.map(function(node){ return node.val }).join('\n');
472
+ filter.attrs = filter.attrs || {};
473
+ filter.attrs.filename = this.options.filename;
383
474
  this.buffer(utils.text(fn(text, filter.attrs)));
384
475
  }
385
476
  },
386
-
477
+
387
478
  /**
388
479
  * Visit `text` node.
389
480
  *
390
481
  * @param {Text} text
391
482
  * @api public
392
483
  */
393
-
484
+
394
485
  visitText: function(text){
395
- text = utils.text(text.nodes.join(''));
486
+ text = utils.text(text.val.replace(/\\/g, '\\\\'));
396
487
  if (this.escape) text = escape(text);
397
488
  this.buffer(text);
398
- this.buffer('\\n');
399
489
  },
400
-
490
+
401
491
  /**
402
492
  * Visit a `comment`, only buffering when the buffer flag is set.
403
493
  *
404
494
  * @param {Comment} comment
405
495
  * @api public
406
496
  */
407
-
497
+
408
498
  visitComment: function(comment){
409
499
  if (!comment.buffer) return;
410
- if (this.pp) this.buffer('\\n' + Array(this.indents + 1).join(' '));
500
+ if (this.pp) this.prettyIndent(1, true);
411
501
  this.buffer('<!--' + utils.escape(comment.val) + '-->');
412
502
  },
413
-
503
+
414
504
  /**
415
505
  * Visit a `BlockComment`.
416
506
  *
417
507
  * @param {Comment} comment
418
508
  * @api public
419
509
  */
420
-
510
+
421
511
  visitBlockComment: function(comment){
422
512
  if (!comment.buffer) return;
423
513
  if (0 == comment.val.trim().indexOf('if')) {
@@ -430,7 +520,7 @@ Compiler.prototype = {
430
520
  this.buffer('-->');
431
521
  }
432
522
  },
433
-
523
+
434
524
  /**
435
525
  * Visit `code`, respecting buffer / escape flags.
436
526
  * If the code is followed by a block, wrap it in
@@ -439,7 +529,7 @@ Compiler.prototype = {
439
529
  * @param {Code} code
440
530
  * @api public
441
531
  */
442
-
532
+
443
533
  visitCode: function(code){
444
534
  // Wrap code blocks with {}.
445
535
  // we only wrap unbuffered code blocks ATM
@@ -463,26 +553,39 @@ Compiler.prototype = {
463
553
  if (!code.buffer) this.buf.push('}');
464
554
  }
465
555
  },
466
-
556
+
467
557
  /**
468
558
  * Visit `each` block.
469
559
  *
470
560
  * @param {Each} each
471
561
  * @api public
472
562
  */
473
-
563
+
474
564
  visitEach: function(each){
475
565
  this.buf.push(''
476
566
  + '// iterate ' + each.obj + '\n'
477
- + '(function(){\n'
478
- + ' if (\'number\' == typeof ' + each.obj + '.length) {\n'
567
+ + ';(function(){\n'
568
+ + ' if (\'number\' == typeof ' + each.obj + '.length) {\n');
569
+
570
+ if (each.alternative) {
571
+ this.buf.push(' if (' + each.obj + '.length) {');
572
+ }
573
+
574
+ this.buf.push(''
479
575
  + ' for (var ' + each.key + ' = 0, $$l = ' + each.obj + '.length; ' + each.key + ' < $$l; ' + each.key + '++) {\n'
480
576
  + ' var ' + each.val + ' = ' + each.obj + '[' + each.key + '];\n');
481
577
 
482
578
  this.visit(each.block);
483
579
 
580
+ this.buf.push(' }\n');
581
+
582
+ if (each.alternative) {
583
+ this.buf.push(' } else {');
584
+ this.visit(each.alternative);
585
+ this.buf.push(' }');
586
+ }
587
+
484
588
  this.buf.push(''
485
- + ' }\n'
486
589
  + ' } else {\n'
487
590
  + ' for (var ' + each.key + ' in ' + each.obj + ') {\n'
488
591
  + ' if (' + each.obj + '.hasOwnProperty(' + each.key + ')){'
@@ -494,21 +597,43 @@ Compiler.prototype = {
494
597
 
495
598
  this.buf.push(' }\n }\n}).call(this);\n');
496
599
  },
497
-
600
+
498
601
  /**
499
602
  * Visit `attrs`.
500
603
  *
501
604
  * @param {Array} attrs
502
605
  * @api public
503
606
  */
504
-
607
+
505
608
  visitAttributes: function(attrs){
609
+ var val = this.attrs(attrs);
610
+ if (val.inherits) {
611
+ this.buf.push("buf.push(attrs(merge({ " + val.buf +
612
+ " }, attributes), merge(" + val.escaped + ", escaped, true)));");
613
+ } else if (val.constant) {
614
+ eval('var buf={' + val.buf + '};');
615
+ this.buffer(runtime.attrs(buf, JSON.parse(val.escaped)), true);
616
+ } else {
617
+ this.buf.push("buf.push(attrs({ " + val.buf + " }, " + val.escaped + "));");
618
+ }
619
+ },
620
+
621
+ /**
622
+ * Compile attributes.
623
+ */
624
+
625
+ attrs: function(attrs){
506
626
  var buf = []
507
- , classes = [];
627
+ , classes = []
628
+ , escaped = {}
629
+ , constant = attrs.every(function(attr){ return isConstant(attr.val) })
630
+ , inherits = false;
508
631
 
509
632
  if (this.terse) buf.push('terse: true');
510
633
 
511
634
  attrs.forEach(function(attr){
635
+ if (attr.name == 'attributes') return inherits = true;
636
+ escaped[attr.name] = attr.escaped;
512
637
  if (attr.name == 'class') {
513
638
  classes.push('(' + attr.val + ')');
514
639
  } else {
@@ -522,12 +647,40 @@ Compiler.prototype = {
522
647
  buf.push("class: " + classes);
523
648
  }
524
649
 
525
- buf = buf.join(', ').replace('class:', '"class":');
526
-
527
- this.buf.push("buf.push(attrs({ " + buf + " }));");
650
+ return {
651
+ buf: buf.join(', ').replace('class:', '"class":'),
652
+ escaped: JSON.stringify(escaped),
653
+ inherits: inherits,
654
+ constant: constant
655
+ };
528
656
  }
529
657
  };
530
658
 
659
+ /**
660
+ * Check if expression can be evaluated to a constant
661
+ *
662
+ * @param {String} expression
663
+ * @return {Boolean}
664
+ * @api private
665
+ */
666
+
667
+ function isConstant(val){
668
+ // Check strings/literals
669
+ if (/^ *("([^"\\]*(\\.[^"\\]*)*)"|'([^'\\]*(\\.[^'\\]*)*)'|true|false|null|undefined) *$/i.test(val))
670
+ return true;
671
+
672
+ // Check numbers
673
+ if (!isNaN(Number(val)))
674
+ return true;
675
+
676
+ // Check arrays
677
+ var matches;
678
+ if (matches = /^ *\[(.*)\] *$/.exec(val))
679
+ return matches[1].split(',').every(isConstant);
680
+
681
+ return false;
682
+ }
683
+
531
684
  /**
532
685
  * Escape the given string of `html`.
533
686
  *
@@ -556,8 +709,8 @@ require.register("doctypes.js", function(module, exports, require){
556
709
 
557
710
  module.exports = {
558
711
  '5': '<!DOCTYPE html>'
712
+ , 'default': '<!DOCTYPE html>'
559
713
  , 'xml': '<?xml version="1.0" encoding="utf-8" ?>'
560
- , 'default': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
561
714
  , 'transitional': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
562
715
  , 'strict': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
563
716
  , 'frameset': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
@@ -661,7 +814,7 @@ module.exports = {
661
814
 
662
815
  coffeescript: function(str){
663
816
  str = str.replace(/\\n/g, '\n');
664
- var js = require('coffee-script').compile(str).replace(/\n/g, '\\n');
817
+ var js = require('coffee-script').compile(str).replace(/\\/g, '\\\\').replace(/\n/g, '\\n');
665
818
  return '<script type="text/javascript">\\n' + js + '</script>';
666
819
  }
667
820
  };
@@ -719,7 +872,7 @@ var Parser = require('./parser')
719
872
  * Library version.
720
873
  */
721
874
 
722
- exports.version = '0.20.0';
875
+ exports.version = '0.27.2';
723
876
 
724
877
  /**
725
878
  * Expose self closing tags.
@@ -770,7 +923,7 @@ exports.Lexer = Lexer;
770
923
  exports.nodes = require('./nodes');
771
924
 
772
925
  /**
773
- * Jade runtime mixins.
926
+ * Jade runtime helpers.
774
927
  */
775
928
 
776
929
  exports.runtime = runtime;
@@ -817,10 +970,24 @@ function parse(str, options){
817
970
  }
818
971
 
819
972
  /**
820
- * Precompile a string representation of the given jade `str`.
973
+ * Strip any UTF-8 BOM off of the start of `str`, if it exists.
974
+ *
975
+ * @param {String} str
976
+ * @return {String}
977
+ * @api private
978
+ */
979
+
980
+ function stripBOM(str){
981
+ return 0xFEFF == str.charCodeAt(0)
982
+ ? str.substring(1)
983
+ : str;
984
+ }
985
+
986
+ /**
987
+ * Compile a `Function` representation of the given jade `str`.
821
988
  *
822
989
  * Options:
823
- *
990
+ *
824
991
  * - `compileDebug` when `false` debugging code is stripped from the compiled template
825
992
  * - `client` when `true` the helper functions `escape()` etc will reference `jade.escape()`
826
993
  * for use with the Jade client-side runtime.js
@@ -831,7 +998,7 @@ function parse(str, options){
831
998
  * @api public
832
999
  */
833
1000
 
834
- exports.precompile = function(str, options){
1001
+ exports.compile = function(str, options){
835
1002
  var options = options || {}
836
1003
  , client = options.client
837
1004
  , filename = options.filename
@@ -839,52 +1006,31 @@ exports.precompile = function(str, options){
839
1006
  : 'undefined'
840
1007
  , fn;
841
1008
 
1009
+ str = stripBOM(String(str));
1010
+
842
1011
  if (options.compileDebug !== false) {
843
1012
  fn = [
844
1013
  'var __jade = [{ lineno: 1, filename: ' + filename + ' }];'
845
1014
  , 'try {'
846
- , parse(String(str), options)
1015
+ , parse(str, options)
847
1016
  , '} catch (err) {'
848
1017
  , ' rethrow(err, __jade[0].filename, __jade[0].lineno);'
849
1018
  , '}'
850
1019
  ].join('\n');
851
1020
  } else {
852
- fn = parse(String(str), options);
1021
+ fn = parse(str, options);
853
1022
  }
854
1023
 
855
1024
  if (client) {
856
- fn = 'var attrs = jade.attrs, escape = jade.escape, rethrow = jade.rethrow;\n' + fn;
1025
+ fn = 'attrs = attrs || jade.attrs; escape = escape || jade.escape; rethrow = rethrow || jade.rethrow; merge = merge || jade.merge;\n' + fn;
857
1026
  }
858
-
859
- return fn;
860
- }
861
1027
 
862
- /**
863
- * Compile a `Function` representation of the given jade `str`.
864
- *
865
- * Options:
866
- *
867
- * - `compileDebug` when `false` debugging code is stripped from the compiled template
868
- * - `client` when `true` the helper functions `escape()` etc will reference `jade.escape()`
869
- * for use with the Jade client-side runtime.js
870
- *
871
- * @param {String} str
872
- * @param {Options} options
873
- * @return {Function}
874
- * @api public
875
- */
876
-
877
- exports.compile = function(str, options){
878
- var options = options || {}
879
- , client = options.client,
880
- fn;
881
-
882
- fn = new Function('locals, attrs, escape, rethrow', exports.precompile(str, options));
1028
+ fn = new Function('locals, attrs, escape, rethrow, merge', fn);
883
1029
 
884
1030
  if (client) return fn;
885
1031
 
886
1032
  return function(locals){
887
- return fn(locals, runtime.attrs, runtime.escape, runtime.rethrow);
1033
+ return fn(locals, runtime.attrs, runtime.escape, runtime.rethrow, runtime.merge);
888
1034
  };
889
1035
  };
890
1036
 
@@ -968,6 +1114,8 @@ require.register("lexer.js", function(module, exports, require){
968
1114
  * MIT Licensed
969
1115
  */
970
1116
 
1117
+ var utils = require('./utils');
1118
+
971
1119
  /**
972
1120
  * Initialize `Lexer` with the given `str`.
973
1121
  *
@@ -1084,9 +1232,9 @@ Lexer.prototype = {
1084
1232
  , nend = 0
1085
1233
  , pos = 0;
1086
1234
  for (var i = 0, len = str.length; i < len; ++i) {
1087
- if (start == str[i]) {
1235
+ if (start == str.charAt(i)) {
1088
1236
  ++nstart;
1089
- } else if (end == str[i]) {
1237
+ } else if (end == str.charAt(i)) {
1090
1238
  if (++nend == nstart) {
1091
1239
  pos = i;
1092
1240
  break;
@@ -1128,6 +1276,19 @@ Lexer.prototype = {
1128
1276
  }
1129
1277
  },
1130
1278
 
1279
+ /**
1280
+ * Blank line.
1281
+ */
1282
+
1283
+ blank: function() {
1284
+ var captures;
1285
+ if (captures = /^\n *\n/.exec(this.input)) {
1286
+ this.consume(captures[0].length - 1);
1287
+ if (this.pipeless) return this.tok('text', '');
1288
+ return this.next();
1289
+ }
1290
+ },
1291
+
1131
1292
  /**
1132
1293
  * Comment.
1133
1294
  */
@@ -1141,14 +1302,26 @@ Lexer.prototype = {
1141
1302
  return tok;
1142
1303
  }
1143
1304
  },
1144
-
1305
+
1306
+ /**
1307
+ * Interpolated tag.
1308
+ */
1309
+
1310
+ interpolation: function() {
1311
+ var captures;
1312
+ if (captures = /^#\{(.*?)\}/.exec(this.input)) {
1313
+ this.consume(captures[0].length);
1314
+ return this.tok('interpolation', captures[1]);
1315
+ }
1316
+ },
1317
+
1145
1318
  /**
1146
1319
  * Tag.
1147
1320
  */
1148
1321
 
1149
1322
  tag: function() {
1150
1323
  var captures;
1151
- if (captures = /^(\w[-:\w]*)/.exec(this.input)) {
1324
+ if (captures = /^(\w[-:\w]*)(\/?)/.exec(this.input)) {
1152
1325
  this.consume(captures[0].length);
1153
1326
  var tok, name = captures[1];
1154
1327
  if (':' == name[name.length - 1]) {
@@ -1159,6 +1332,7 @@ Lexer.prototype = {
1159
1332
  } else {
1160
1333
  tok = this.tok('tag', name);
1161
1334
  }
1335
+ tok.selfClosing = !! captures[2];
1162
1336
  return tok;
1163
1337
  }
1164
1338
  },
@@ -1200,15 +1374,15 @@ Lexer.prototype = {
1200
1374
  */
1201
1375
 
1202
1376
  text: function() {
1203
- return this.scan(/^(?:\| ?)?([^\n]+)/, 'text');
1377
+ return this.scan(/^(?:\| ?| ?)?([^\n]+)/, 'text');
1204
1378
  },
1205
1379
 
1206
1380
  /**
1207
1381
  * Extends.
1208
1382
  */
1209
1383
 
1210
- extends: function() {
1211
- return this.scan(/^extends +([^\n]+)/, 'extends');
1384
+ "extends": function() {
1385
+ return this.scan(/^extends? +([^\n]+)/, 'extends');
1212
1386
  },
1213
1387
 
1214
1388
  /**
@@ -1249,11 +1423,12 @@ Lexer.prototype = {
1249
1423
 
1250
1424
  block: function() {
1251
1425
  var captures;
1252
- if (captures = /^block +(?:(prepend|append) +)?([^\n]+)/.exec(this.input)) {
1426
+ if (captures = /^block\b *(?:(prepend|append) +)?([^\n]*)/.exec(this.input)) {
1253
1427
  this.consume(captures[0].length);
1254
1428
  var mode = captures[1] || 'replace'
1255
1429
  , name = captures[2]
1256
1430
  , tok = this.tok('block', name);
1431
+
1257
1432
  tok.mode = mode;
1258
1433
  return tok;
1259
1434
  }
@@ -1279,7 +1454,7 @@ Lexer.prototype = {
1279
1454
  * Case.
1280
1455
  */
1281
1456
 
1282
- case: function() {
1457
+ "case": function() {
1283
1458
  return this.scan(/^case +([^\n]+)/, 'case');
1284
1459
  },
1285
1460
 
@@ -1295,7 +1470,7 @@ Lexer.prototype = {
1295
1470
  * Default.
1296
1471
  */
1297
1472
 
1298
- default: function() {
1473
+ "default": function() {
1299
1474
  return this.scan(/^default */, 'default');
1300
1475
  },
1301
1476
 
@@ -1313,6 +1488,28 @@ Lexer.prototype = {
1313
1488
  }
1314
1489
  },
1315
1490
 
1491
+ /**
1492
+ * Call mixin.
1493
+ */
1494
+
1495
+ call: function(){
1496
+ var captures;
1497
+ if (captures = /^\+([-\w]+)/.exec(this.input)) {
1498
+ this.consume(captures[0].length);
1499
+ var tok = this.tok('call', captures[1]);
1500
+
1501
+ // Check for args (not attributes)
1502
+ if (captures = /^ *\((.*?)\)/.exec(this.input)) {
1503
+ if (!/^ *[-\w]+ *=/.test(captures[1])) {
1504
+ this.consume(captures[0].length);
1505
+ tok.args = captures[1];
1506
+ }
1507
+ }
1508
+
1509
+ return tok;
1510
+ }
1511
+ },
1512
+
1316
1513
  /**
1317
1514
  * Mixin.
1318
1515
  */
@@ -1353,7 +1550,7 @@ Lexer.prototype = {
1353
1550
  * While.
1354
1551
  */
1355
1552
 
1356
- while: function() {
1553
+ "while": function() {
1357
1554
  var captures;
1358
1555
  if (captures = /^while +([^\n]+)/.exec(this.input)) {
1359
1556
  this.consume(captures[0].length);
@@ -1398,17 +1595,19 @@ Lexer.prototype = {
1398
1595
  */
1399
1596
 
1400
1597
  attrs: function() {
1401
- if ('(' == this.input[0]) {
1598
+ if ('(' == this.input.charAt(0)) {
1402
1599
  var index = this.indexOfDelimiters('(', ')')
1403
1600
  , str = this.input.substr(1, index-1)
1404
1601
  , tok = this.tok('attrs')
1405
1602
  , len = str.length
1406
1603
  , colons = this.colons
1407
1604
  , states = ['key']
1605
+ , escapedAttr
1408
1606
  , key = ''
1409
1607
  , val = ''
1410
1608
  , quote
1411
- , c;
1609
+ , c
1610
+ , p;
1412
1611
 
1413
1612
  function state(){
1414
1613
  return states[states.length - 1];
@@ -1422,6 +1621,7 @@ Lexer.prototype = {
1422
1621
 
1423
1622
  this.consume(index + 1);
1424
1623
  tok.attrs = {};
1624
+ tok.escaped = {};
1425
1625
 
1426
1626
  function parse(c) {
1427
1627
  var real = c;
@@ -1442,7 +1642,9 @@ Lexer.prototype = {
1442
1642
  val = val.trim();
1443
1643
  key = key.trim();
1444
1644
  if ('' == key) return;
1445
- tok.attrs[key.replace(/^['"]|['"]$/g, '')] = '' == val
1645
+ key = key.replace(/^['"]|['"]$/g, '').replace('!', '');
1646
+ tok.escaped[key] = escapedAttr;
1647
+ tok.attrs[key] = '' == val
1446
1648
  ? true
1447
1649
  : interpolate(val);
1448
1650
  key = val = '';
@@ -1461,6 +1663,7 @@ Lexer.prototype = {
1461
1663
  val += real;
1462
1664
  break;
1463
1665
  default:
1666
+ escapedAttr = '!' != p;
1464
1667
  states.push('val');
1465
1668
  }
1466
1669
  break;
@@ -1521,14 +1724,20 @@ Lexer.prototype = {
1521
1724
  val += c;
1522
1725
  }
1523
1726
  }
1727
+ p = c;
1524
1728
  }
1525
1729
 
1526
1730
  for (var i = 0; i < len; ++i) {
1527
- parse(str[i]);
1731
+ parse(str.charAt(i));
1528
1732
  }
1529
1733
 
1530
1734
  parse(',');
1531
1735
 
1736
+ if ('/' == this.input.charAt(0)) {
1737
+ this.consume(1);
1738
+ tok.selfClosing = true;
1739
+ }
1740
+
1532
1741
  return tok;
1533
1742
  }
1534
1743
  },
@@ -1639,22 +1848,25 @@ Lexer.prototype = {
1639
1848
 
1640
1849
  next: function() {
1641
1850
  return this.deferred()
1851
+ || this.blank()
1642
1852
  || this.eos()
1643
1853
  || this.pipelessText()
1644
1854
  || this.yield()
1645
1855
  || this.doctype()
1646
- || this.case()
1856
+ || this.interpolation()
1857
+ || this["case"]()
1647
1858
  || this.when()
1648
- || this.default()
1649
- || this.extends()
1859
+ || this["default"]()
1860
+ || this["extends"]()
1650
1861
  || this.append()
1651
1862
  || this.prepend()
1652
1863
  || this.block()
1653
1864
  || this.include()
1654
1865
  || this.mixin()
1866
+ || this.call()
1655
1867
  || this.conditional()
1656
1868
  || this.each()
1657
- || this.while()
1869
+ || this["while"]()
1658
1870
  || this.assignment()
1659
1871
  || this.tag()
1660
1872
  || this.filter()
@@ -1671,6 +1883,89 @@ Lexer.prototype = {
1671
1883
 
1672
1884
  }); // module: lexer.js
1673
1885
 
1886
+ require.register("nodes/attrs.js", function(module, exports, require){
1887
+
1888
+ /*!
1889
+ * Jade - nodes - Attrs
1890
+ * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
1891
+ * MIT Licensed
1892
+ */
1893
+
1894
+ /**
1895
+ * Module dependencies.
1896
+ */
1897
+
1898
+ var Node = require('./node'),
1899
+ Block = require('./block');
1900
+
1901
+ /**
1902
+ * Initialize a `Attrs` node.
1903
+ *
1904
+ * @api public
1905
+ */
1906
+
1907
+ var Attrs = module.exports = function Attrs() {
1908
+ this.attrs = [];
1909
+ };
1910
+
1911
+ /**
1912
+ * Inherit from `Node`.
1913
+ */
1914
+
1915
+ Attrs.prototype = new Node;
1916
+ Attrs.prototype.constructor = Attrs;
1917
+
1918
+
1919
+ /**
1920
+ * Set attribute `name` to `val`, keep in mind these become
1921
+ * part of a raw js object literal, so to quote a value you must
1922
+ * '"quote me"', otherwise or example 'user.name' is literal JavaScript.
1923
+ *
1924
+ * @param {String} name
1925
+ * @param {String} val
1926
+ * @param {Boolean} escaped
1927
+ * @return {Tag} for chaining
1928
+ * @api public
1929
+ */
1930
+
1931
+ Attrs.prototype.setAttribute = function(name, val, escaped){
1932
+ this.attrs.push({ name: name, val: val, escaped: escaped });
1933
+ return this;
1934
+ };
1935
+
1936
+ /**
1937
+ * Remove attribute `name` when present.
1938
+ *
1939
+ * @param {String} name
1940
+ * @api public
1941
+ */
1942
+
1943
+ Attrs.prototype.removeAttribute = function(name){
1944
+ for (var i = 0, len = this.attrs.length; i < len; ++i) {
1945
+ if (this.attrs[i] && this.attrs[i].name == name) {
1946
+ delete this.attrs[i];
1947
+ }
1948
+ }
1949
+ };
1950
+
1951
+ /**
1952
+ * Get attribute value by `name`.
1953
+ *
1954
+ * @param {String} name
1955
+ * @return {String}
1956
+ * @api public
1957
+ */
1958
+
1959
+ Attrs.prototype.getAttribute = function(name){
1960
+ for (var i = 0, len = this.attrs.length; i < len; ++i) {
1961
+ if (this.attrs[i] && this.attrs[i].name == name) {
1962
+ return this.attrs[i].val;
1963
+ }
1964
+ }
1965
+ };
1966
+
1967
+ }); // module: nodes/attrs.js
1968
+
1674
1969
  require.register("nodes/block-comment.js", function(module, exports, require){
1675
1970
 
1676
1971
  /*!
@@ -1743,6 +2038,12 @@ Block.prototype = new Node;
1743
2038
  Block.prototype.constructor = Block;
1744
2039
 
1745
2040
 
2041
+ /**
2042
+ * Block flag.
2043
+ */
2044
+
2045
+ Block.prototype.isBlock = true;
2046
+
1746
2047
  /**
1747
2048
  * Replace the nodes in `other` with the nodes
1748
2049
  * in `this` block.
@@ -1812,6 +2113,21 @@ Block.prototype.includeBlock = function(){
1812
2113
  return ret;
1813
2114
  };
1814
2115
 
2116
+ /**
2117
+ * Return a clone of this block.
2118
+ *
2119
+ * @return {Block}
2120
+ * @api private
2121
+ */
2122
+
2123
+ Block.prototype.clone = function(){
2124
+ var clone = new Block;
2125
+ for (var i = 0, len = this.nodes.length; i < len; ++i) {
2126
+ clone.push(this.nodes[i].clone());
2127
+ }
2128
+ return clone;
2129
+ };
2130
+
1815
2131
 
1816
2132
  }); // module: nodes/block.js
1817
2133
 
@@ -2045,7 +2361,7 @@ var Filter = module.exports = function Filter(name, block, attrs) {
2045
2361
  this.name = name;
2046
2362
  this.block = block;
2047
2363
  this.attrs = attrs;
2048
- this.isASTFilter = block instanceof Block;
2364
+ this.isASTFilter = !block.nodes.every(function(node){ return node.isText });
2049
2365
  };
2050
2366
 
2051
2367
  /**
@@ -2104,7 +2420,8 @@ var Node = require('./node');
2104
2420
 
2105
2421
  var Literal = module.exports = function Literal(str) {
2106
2422
  this.str = str
2107
- .replace(/\n/g, "\\n")
2423
+ .replace(/\\/g, "\\\\")
2424
+ .replace(/\n|\r\n/g, "\\n")
2108
2425
  .replace(/'/g, "\\'");
2109
2426
  };
2110
2427
 
@@ -2130,7 +2447,7 @@ require.register("nodes/mixin.js", function(module, exports, require){
2130
2447
  * Module dependencies.
2131
2448
  */
2132
2449
 
2133
- var Node = require('./node');
2450
+ var Attrs = require('./attrs');
2134
2451
 
2135
2452
  /**
2136
2453
  * Initialize a new `Mixin` with `name` and `block`.
@@ -2141,17 +2458,19 @@ var Node = require('./node');
2141
2458
  * @api public
2142
2459
  */
2143
2460
 
2144
- var Mixin = module.exports = function Mixin(name, args, block){
2461
+ var Mixin = module.exports = function Mixin(name, args, block, call){
2145
2462
  this.name = name;
2146
2463
  this.args = args;
2147
2464
  this.block = block;
2465
+ this.attrs = [];
2466
+ this.call = call;
2148
2467
  };
2149
2468
 
2150
2469
  /**
2151
- * Inherit from `Node`.
2470
+ * Inherit from `Attrs`.
2152
2471
  */
2153
2472
 
2154
- Mixin.prototype = new Node;
2473
+ Mixin.prototype = new Attrs;
2155
2474
  Mixin.prototype.constructor = Mixin;
2156
2475
 
2157
2476
 
@@ -2173,6 +2492,18 @@ require.register("nodes/node.js", function(module, exports, require){
2173
2492
  */
2174
2493
 
2175
2494
  var Node = module.exports = function Node(){};
2495
+
2496
+ /**
2497
+ * Clone this node (return itself)
2498
+ *
2499
+ * @return {Node}
2500
+ * @api private
2501
+ */
2502
+
2503
+ Node.prototype.clone = function(){
2504
+ return this;
2505
+ };
2506
+
2176
2507
  }); // module: nodes/node.js
2177
2508
 
2178
2509
  require.register("nodes/tag.js", function(module, exports, require){
@@ -2187,8 +2518,9 @@ require.register("nodes/tag.js", function(module, exports, require){
2187
2518
  * Module dependencies.
2188
2519
  */
2189
2520
 
2190
- var Node = require('./node'),
2191
- Block = require('./block');
2521
+ var Attrs = require('./attrs'),
2522
+ Block = require('./block'),
2523
+ inlineTags = require('../inline-tags');
2192
2524
 
2193
2525
  /**
2194
2526
  * Initialize a `Tag` node with the given tag `name` and optional `block`.
@@ -2205,60 +2537,73 @@ var Tag = module.exports = function Tag(name, block) {
2205
2537
  };
2206
2538
 
2207
2539
  /**
2208
- * Inherit from `Node`.
2540
+ * Inherit from `Attrs`.
2209
2541
  */
2210
2542
 
2211
- Tag.prototype = new Node;
2543
+ Tag.prototype = new Attrs;
2212
2544
  Tag.prototype.constructor = Tag;
2213
2545
 
2214
2546
 
2215
2547
  /**
2216
- * Set attribute `name` to `val`, keep in mind these become
2217
- * part of a raw js object literal, so to quote a value you must
2218
- * '"quote me"', otherwise or example 'user.name' is literal JavaScript.
2548
+ * Clone this tag.
2219
2549
  *
2220
- * @param {String} name
2221
- * @param {String} val
2222
- * @return {Tag} for chaining
2223
- * @api public
2550
+ * @return {Tag}
2551
+ * @api private
2224
2552
  */
2225
2553
 
2226
- Tag.prototype.setAttribute = function(name, val){
2227
- this.attrs.push({ name: name, val: val });
2228
- return this;
2554
+ Tag.prototype.clone = function(){
2555
+ var clone = new Tag(this.name, this.block.clone());
2556
+ clone.line = this.line;
2557
+ clone.attrs = this.attrs;
2558
+ clone.textOnly = this.textOnly;
2559
+ return clone;
2229
2560
  };
2230
2561
 
2231
2562
  /**
2232
- * Remove attribute `name` when present.
2563
+ * Check if this tag is an inline tag.
2233
2564
  *
2234
- * @param {String} name
2235
- * @api public
2565
+ * @return {Boolean}
2566
+ * @api private
2236
2567
  */
2237
2568
 
2238
- Tag.prototype.removeAttribute = function(name){
2239
- for (var i = 0, len = this.attrs.length; i < len; ++i) {
2240
- if (this.attrs[i] && this.attrs[i].name == name) {
2241
- delete this.attrs[i];
2242
- }
2243
- }
2569
+ Tag.prototype.isInline = function(){
2570
+ return ~inlineTags.indexOf(this.name);
2244
2571
  };
2245
2572
 
2246
2573
  /**
2247
- * Get attribute value by `name`.
2574
+ * Check if this tag's contents can be inlined. Used for pretty printing.
2248
2575
  *
2249
- * @param {String} name
2250
- * @return {String}
2251
- * @api public
2576
+ * @return {Boolean}
2577
+ * @api private
2252
2578
  */
2253
2579
 
2254
- Tag.prototype.getAttribute = function(name){
2255
- for (var i = 0, len = this.attrs.length; i < len; ++i) {
2256
- if (this.attrs[i] && this.attrs[i].name == name) {
2257
- return this.attrs[i].val;
2580
+ Tag.prototype.canInline = function(){
2581
+ var nodes = this.block.nodes;
2582
+
2583
+ function isInline(node){
2584
+ // Recurse if the node is a block
2585
+ if (node.isBlock) return node.nodes.every(isInline);
2586
+ return node.isText || (node.isInline && node.isInline());
2587
+ }
2588
+
2589
+ // Empty tag
2590
+ if (!nodes.length) return true;
2591
+
2592
+ // Text-only or inline-only tag
2593
+ if (1 == nodes.length) return isInline(nodes[0]);
2594
+
2595
+ // Multi-line inline-only tag
2596
+ if (this.block.nodes.every(isInline)) {
2597
+ for (var i = 1, len = nodes.length; i < len; ++i) {
2598
+ if (nodes[i-1].isText && nodes[i].isText)
2599
+ return false;
2258
2600
  }
2601
+ return true;
2259
2602
  }
2603
+
2604
+ // Mixed tag
2605
+ return false;
2260
2606
  };
2261
-
2262
2607
  }); // module: nodes/tag.js
2263
2608
 
2264
2609
  require.register("nodes/text.js", function(module, exports, require){
@@ -2283,8 +2628,8 @@ var Node = require('./node');
2283
2628
  */
2284
2629
 
2285
2630
  var Text = module.exports = function Text(line) {
2286
- this.nodes = [];
2287
- if ('string' == typeof line) this.push(line);
2631
+ this.val = '';
2632
+ if ('string' == typeof line) this.val = line;
2288
2633
  };
2289
2634
 
2290
2635
  /**
@@ -2296,17 +2641,10 @@ Text.prototype.constructor = Text;
2296
2641
 
2297
2642
 
2298
2643
  /**
2299
- * Push the given `node.`
2300
- *
2301
- * @param {Node} node
2302
- * @return {Number}
2303
- * @api public
2644
+ * Flag as text.
2304
2645
  */
2305
2646
 
2306
- Text.prototype.push = function(node){
2307
- return this.nodes.push(node);
2308
- };
2309
-
2647
+ Text.prototype.isText = true;
2310
2648
  }); // module: nodes/text.js
2311
2649
 
2312
2650
  require.register("parser.js", function(module, exports, require){
@@ -2338,6 +2676,7 @@ var Parser = exports = module.exports = function Parser(str, filename, options){
2338
2676
  this.lexer = new Lexer(str, options);
2339
2677
  this.filename = filename;
2340
2678
  this.blocks = {};
2679
+ this.mixins = {};
2341
2680
  this.options = options;
2342
2681
  this.contexts = [this];
2343
2682
  };
@@ -2446,6 +2785,9 @@ Parser.prototype = {
2446
2785
  this.context(parser);
2447
2786
  var ast = parser.parse();
2448
2787
  this.context();
2788
+ // hoist mixins
2789
+ for (var name in this.mixins)
2790
+ ast.unshift(this.mixins[name]);
2449
2791
  return ast;
2450
2792
  }
2451
2793
 
@@ -2493,6 +2835,7 @@ Parser.prototype = {
2493
2835
  * | yield
2494
2836
  * | id
2495
2837
  * | class
2838
+ * | interpolation
2496
2839
  */
2497
2840
 
2498
2841
  parseExpr: function(){
@@ -2525,6 +2868,10 @@ Parser.prototype = {
2525
2868
  return this.parseEach();
2526
2869
  case 'code':
2527
2870
  return this.parseCode();
2871
+ case 'call':
2872
+ return this.parseCall();
2873
+ case 'interpolation':
2874
+ return this.parseInterpolation();
2528
2875
  case 'yield':
2529
2876
  this.advance();
2530
2877
  var block = new nodes.Block;
@@ -2688,6 +3035,10 @@ Parser.prototype = {
2688
3035
  , node = new nodes.Each(tok.code, tok.val, tok.key);
2689
3036
  node.line = this.line();
2690
3037
  node.block = this.block();
3038
+ if (this.peek().type == 'code' && this.peek().val == 'else') {
3039
+ this.advance();
3040
+ node.alternative = this.block();
3041
+ }
2691
3042
  return node;
2692
3043
  },
2693
3044
 
@@ -2784,6 +3135,8 @@ Parser.prototype = {
2784
3135
  var path = join(dir, path)
2785
3136
  , str = fs.readFileSync(path, 'utf8')
2786
3137
  , parser = new Parser(str, path, this.options);
3138
+ parser.blocks = this.blocks;
3139
+ parser.mixins = this.mixins;
2787
3140
 
2788
3141
  this.context(parser);
2789
3142
  var ast = parser.parse();
@@ -2797,6 +3150,21 @@ Parser.prototype = {
2797
3150
  return ast;
2798
3151
  },
2799
3152
 
3153
+ /**
3154
+ * call ident block
3155
+ */
3156
+
3157
+ parseCall: function(){
3158
+ var tok = this.expect('call')
3159
+ , name = tok.val
3160
+ , args = tok.args
3161
+ , mixin = new nodes.Mixin(name, args, new nodes.Block, true);
3162
+
3163
+ this.tag(mixin);
3164
+ if (mixin.block.isEmpty()) mixin.block = null;
3165
+ return mixin;
3166
+ },
3167
+
2800
3168
  /**
2801
3169
  * mixin block
2802
3170
  */
@@ -2804,11 +3172,18 @@ Parser.prototype = {
2804
3172
  parseMixin: function(){
2805
3173
  var tok = this.expect('mixin')
2806
3174
  , name = tok.val
2807
- , args = tok.args;
2808
- var block = 'indent' == this.peek().type
2809
- ? this.block()
2810
- : null;
2811
- return new nodes.Mixin(name, args, block);
3175
+ , args = tok.args
3176
+ , mixin;
3177
+
3178
+ // definition
3179
+ if ('indent' == this.peek().type) {
3180
+ mixin = new nodes.Mixin(name, args, this.block(), false);
3181
+ this.mixins[name] = mixin;
3182
+ return mixin;
3183
+ // call
3184
+ } else {
3185
+ return new nodes.Mixin(name, args, null, true);
3186
+ }
2812
3187
  },
2813
3188
 
2814
3189
  /**
@@ -2816,32 +3191,31 @@ Parser.prototype = {
2816
3191
  */
2817
3192
 
2818
3193
  parseTextBlock: function(){
2819
- var text = new nodes.Text;
2820
- text.line = this.line();
3194
+ var block = new nodes.Block;
3195
+ block.line = this.line();
2821
3196
  var spaces = this.expect('indent').val;
2822
3197
  if (null == this._spaces) this._spaces = spaces;
2823
3198
  var indent = Array(spaces - this._spaces + 1).join(' ');
2824
3199
  while ('outdent' != this.peek().type) {
2825
3200
  switch (this.peek().type) {
2826
3201
  case 'newline':
2827
- text.push('\\n');
2828
3202
  this.advance();
2829
3203
  break;
2830
3204
  case 'indent':
2831
- text.push('\\n');
2832
3205
  this.parseTextBlock().nodes.forEach(function(node){
2833
- text.push(node);
3206
+ block.push(node);
2834
3207
  });
2835
- text.push('\\n');
2836
3208
  break;
2837
3209
  default:
2838
- text.push(indent + this.advance().val);
3210
+ var text = new nodes.Text(indent + this.advance().val);
3211
+ text.line = this.line();
3212
+ block.push(text);
2839
3213
  }
2840
3214
  }
2841
3215
 
2842
3216
  if (spaces == this._spaces) this._spaces = null;
2843
3217
  this.expect('outdent');
2844
- return text;
3218
+ return block;
2845
3219
  },
2846
3220
 
2847
3221
  /**
@@ -2863,6 +3237,17 @@ Parser.prototype = {
2863
3237
  return block;
2864
3238
  },
2865
3239
 
3240
+ /**
3241
+ * interpolation (attrs | class | id)* (text | code | ':')? newline* block?
3242
+ */
3243
+
3244
+ parseInterpolation: function(){
3245
+ var tok = this.advance();
3246
+ var tag = new nodes.Tag(tok.val);
3247
+ tag.buffer = true;
3248
+ return this.tag(tag);
3249
+ },
3250
+
2866
3251
  /**
2867
3252
  * tag (attrs | class | id)* (text | code | ':')? newline* block?
2868
3253
  */
@@ -2877,9 +3262,20 @@ Parser.prototype = {
2877
3262
  }
2878
3263
  }
2879
3264
 
2880
- var name = this.advance().val
2881
- , tag = new nodes.Tag(name)
2882
- , dot;
3265
+ var tok = this.advance()
3266
+ , tag = new nodes.Tag(tok.val);
3267
+
3268
+ tag.selfClosing = tok.selfClosing;
3269
+
3270
+ return this.tag(tag);
3271
+ },
3272
+
3273
+ /**
3274
+ * Parse tag.
3275
+ */
3276
+
3277
+ tag: function(tag){
3278
+ var dot;
2883
3279
 
2884
3280
  tag.line = this.line();
2885
3281
 
@@ -2893,12 +3289,17 @@ Parser.prototype = {
2893
3289
  tag.setAttribute(tok.type, "'" + tok.val + "'");
2894
3290
  continue;
2895
3291
  case 'attrs':
2896
- var obj = this.advance().attrs
3292
+ var tok = this.advance()
3293
+ , obj = tok.attrs
3294
+ , escaped = tok.escaped
2897
3295
  , names = Object.keys(obj);
3296
+
3297
+ if (tok.selfClosing) tag.selfClosing = true;
3298
+
2898
3299
  for (var i = 0, len = names.length; i < len; ++i) {
2899
3300
  var name = names[i]
2900
3301
  , val = obj[name];
2901
- tag.setAttribute(name, val);
3302
+ tag.setAttribute(name, val, escaped[name]);
2902
3303
  }
2903
3304
  continue;
2904
3305
  default:
@@ -2915,7 +3316,7 @@ Parser.prototype = {
2915
3316
  // (text | code | ':')?
2916
3317
  switch (this.peek().type) {
2917
3318
  case 'text':
2918
- tag.text = this.parseText();
3319
+ tag.block.push(this.parseText());
2919
3320
  break;
2920
3321
  case 'code':
2921
3322
  tag.code = this.parseCode();
@@ -2923,7 +3324,7 @@ Parser.prototype = {
2923
3324
  case ':':
2924
3325
  this.advance();
2925
3326
  tag.block = new nodes.Block;
2926
- tag.block.push(this.parseTag());
3327
+ tag.block.push(this.parseExpr());
2927
3328
  break;
2928
3329
  }
2929
3330
 
@@ -2995,41 +3396,97 @@ if (!Object.keys) {
2995
3396
  }
2996
3397
  }
2997
3398
  return arr;
2998
- }
3399
+ }
3400
+ }
3401
+
3402
+ /**
3403
+ * Merge two attribute objects giving precedence
3404
+ * to values in object `b`. Classes are special-cased
3405
+ * allowing for arrays and merging/joining appropriately
3406
+ * resulting in a string.
3407
+ *
3408
+ * @param {Object} a
3409
+ * @param {Object} b
3410
+ * @return {Object} a
3411
+ * @api private
3412
+ */
3413
+
3414
+ exports.merge = function merge(a, b) {
3415
+ var ac = a['class'];
3416
+ var bc = b['class'];
3417
+
3418
+ if (ac || bc) {
3419
+ ac = ac || [];
3420
+ bc = bc || [];
3421
+ if (!Array.isArray(ac)) ac = [ac];
3422
+ if (!Array.isArray(bc)) bc = [bc];
3423
+ ac = ac.filter(nulls);
3424
+ bc = bc.filter(nulls);
3425
+ a['class'] = ac.concat(bc).join(' ');
3426
+ }
3427
+
3428
+ for (var key in b) {
3429
+ if (key != 'class') {
3430
+ a[key] = b[key];
3431
+ }
3432
+ }
3433
+
3434
+ return a;
3435
+ };
3436
+
3437
+ /**
3438
+ * Filter null `val`s.
3439
+ *
3440
+ * @param {Mixed} val
3441
+ * @return {Mixed}
3442
+ * @api private
3443
+ */
3444
+
3445
+ function nulls(val) {
3446
+ return val != null;
2999
3447
  }
3000
3448
 
3001
3449
  /**
3002
3450
  * Render the given attributes object.
3003
3451
  *
3004
3452
  * @param {Object} obj
3453
+ * @param {Object} escaped
3005
3454
  * @return {String}
3006
3455
  * @api private
3007
3456
  */
3008
3457
 
3009
- exports.attrs = function attrs(obj){
3458
+ exports.attrs = function attrs(obj, escaped){
3010
3459
  var buf = []
3011
3460
  , terse = obj.terse;
3461
+
3012
3462
  delete obj.terse;
3013
3463
  var keys = Object.keys(obj)
3014
3464
  , len = keys.length;
3465
+
3015
3466
  if (len) {
3016
3467
  buf.push('');
3017
3468
  for (var i = 0; i < len; ++i) {
3018
3469
  var key = keys[i]
3019
3470
  , val = obj[key];
3471
+
3020
3472
  if ('boolean' == typeof val || null == val) {
3021
3473
  if (val) {
3022
3474
  terse
3023
3475
  ? buf.push(key)
3024
3476
  : buf.push(key + '="' + key + '"');
3025
3477
  }
3478
+ } else if (0 == key.indexOf('data') && 'string' != typeof val) {
3479
+ buf.push(key + "='" + JSON.stringify(val) + "'");
3026
3480
  } else if ('class' == key && Array.isArray(val)) {
3027
3481
  buf.push(key + '="' + exports.escape(val.join(' ')) + '"');
3028
- } else {
3482
+ } else if (escaped && escaped[key]) {
3029
3483
  buf.push(key + '="' + exports.escape(val) + '"');
3484
+ } else {
3485
+ buf.push(key + '="' + val + '"');
3030
3486
  }
3031
3487
  }
3032
3488
  }
3489
+
3033
3490
  return buf.join(' ');
3034
3491
  };
3035
3492
 
@@ -3043,7 +3500,7 @@ exports.attrs = function attrs(obj){
3043
3500
 
3044
3501
  exports.escape = function escape(html){
3045
3502
  return String(html)
3046
- .replace(/&(?!\w+;)/g, '&amp;')
3503
+ .replace(/&(?!(\w+|\#\d+);)/g, '&amp;')
3047
3504
  .replace(/</g, '&lt;')
3048
3505
  .replace(/>/g, '&gt;')
3049
3506
  .replace(/"/g, '&quot;');
@@ -3062,30 +3519,25 @@ exports.escape = function escape(html){
3062
3519
  exports.rethrow = function rethrow(err, filename, lineno){
3063
3520
  if (!filename) throw err;
3064
3521
 
3065
- // If we can't catch the context we still output line and file
3066
- try {
3067
- var context = 3
3068
- , str = require('fs').readFileSync(filename, 'utf8')
3069
- , lines = str.split('\n')
3070
- , start = Math.max(lineno - context, 0)
3071
- , end = Math.min(lines.length, lineno + context);
3072
-
3073
- // Error context
3074
- var context = lines.slice(start, end).map(function(line, i){
3075
- var curr = i + start + 1;
3076
- return (curr == lineno ? ' > ' : ' ')
3077
- + curr
3078
- + '| '
3079
- + line;
3080
- }).join('\n') + '\n\n';
3081
- } catch(failure) {
3082
- var context = '';
3083
- }
3522
+ var context = 3
3523
+ , str = require('fs').readFileSync(filename, 'utf8')
3524
+ , lines = str.split('\n')
3525
+ , start = Math.max(lineno - context, 0)
3526
+ , end = Math.min(lines.length, lineno + context);
3527
+
3528
+ // Error context
3529
+ var context = lines.slice(start, end).map(function(line, i){
3530
+ var curr = i + start + 1;
3531
+ return (curr == lineno ? ' > ' : ' ')
3532
+ + curr
3533
+ + '| '
3534
+ + line;
3535
+ }).join('\n');
3084
3536
 
3085
3537
  // Alter exception message
3086
3538
  err.path = filename;
3087
- err.message = (filename || 'Jade') + ':' + lineno
3088
- + '\n' + context + err.message;
3539
+ err.message = (filename || 'Jade') + ':' + lineno
3540
+ + '\n' + context + '\n\n' + err.message;
3089
3541
  throw err;
3090
3542
  };
3091
3543
 
@@ -3104,6 +3556,7 @@ module.exports = [
3104
3556
  , 'img'
3105
3557
  , 'link'
3106
3558
  , 'input'
3559
+ , 'source'
3107
3560
  , 'area'
3108
3561
  , 'base'
3109
3562
  , 'col'