jader 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
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'