├── spec ├── dummy │ ├── log │ │ └── .gitkeep │ ├── app │ │ ├── mailers │ │ │ └── .gitkeep │ │ ├── models │ │ │ ├── .gitkeep │ │ │ └── user.rb │ │ ├── helpers │ │ │ └── application_helper.rb │ │ ├── assets │ │ │ ├── javascripts │ │ │ │ ├── views │ │ │ │ │ └── users │ │ │ │ │ │ ├── dummy.jst.jade │ │ │ │ │ │ └── index.jade │ │ │ │ ├── mixins │ │ │ │ │ ├── users_mixins.jade │ │ │ │ │ └── application_mixins.jade │ │ │ │ ├── sample.jst.jade │ │ │ │ ├── includes │ │ │ │ │ └── util.js │ │ │ │ └── application.js │ │ │ └── stylesheets │ │ │ │ └── application.css │ │ ├── views │ │ │ ├── users │ │ │ │ └── show.jade │ │ │ └── layouts │ │ │ │ └── application.html.erb │ │ └── controllers │ │ │ ├── application_controller.rb │ │ │ └── users_controller.rb │ ├── lib │ │ └── assets │ │ │ └── .gitkeep │ ├── public │ │ ├── favicon.ico │ │ ├── 500.html │ │ ├── 422.html │ │ └── 404.html │ ├── config │ │ ├── database.yml │ │ ├── environment.rb │ │ ├── locales │ │ │ └── en.yml │ │ ├── initializers │ │ │ ├── mime_types.rb │ │ │ ├── jader.rb │ │ │ ├── wrap_parameters.rb │ │ │ ├── backtrace_silencers.rb │ │ │ ├── session_store.rb │ │ │ ├── secret_token.rb │ │ │ └── inflections.rb │ │ ├── boot.rb │ │ ├── environments │ │ │ ├── development.rb │ │ │ ├── test.rb │ │ │ └── production.rb │ │ ├── routes.rb │ │ └── application.rb │ ├── config.ru │ ├── db │ │ ├── migrate │ │ │ └── 20120816043248_user.rb │ │ └── schema.rb │ ├── Rakefile │ ├── script │ │ └── rails │ └── README.rdoc ├── support │ ├── global.rb │ └── matchers.rb ├── source_spec.rb ├── compiler_spec.rb ├── spec_helper.rb ├── template_spec.rb └── integration │ └── renderer_spec.rb ├── lib ├── jader │ ├── version.rb │ ├── source.rb │ ├── template.rb │ ├── engine.rb │ ├── configuration.rb │ ├── renderer.rb │ ├── serialize.rb │ └── compiler.rb └── jader.rb ├── Rakefile ├── .gitignore ├── Gemfile ├── LICENSE ├── jader.gemspec ├── vendor └── assets │ └── javascripts │ └── jade │ ├── runtime.js │ └── jade.js └── README.md /spec/dummy/log/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/lib/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/jader/version.rb: -------------------------------------------------------------------------------- 1 | module Jader 2 | VERSION = "0.0.10" 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/views/users/dummy.jst.jade: -------------------------------------------------------------------------------- 1 | h1 2 | mixin upcase(phrase) -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new('spec') -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/mixins/users_mixins.jade: -------------------------------------------------------------------------------- 1 | mixin upcase(str) 2 | - cap = str.toUpperCase() 3 | strong= cap -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/sample.jst.jade: -------------------------------------------------------------------------------- 1 | !!!5 2 | head 3 | title Hello, #{name} :) 4 | body 5 | | Yap, it works 6 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/mixins/application_mixins.jade: -------------------------------------------------------------------------------- 1 | mixin downcase(str) 2 | - low = str.toLowerCase() 3 | strong= low -------------------------------------------------------------------------------- /spec/support/global.rb: -------------------------------------------------------------------------------- 1 | def assets 2 | Rails.application.assets 3 | end 4 | 5 | def asset_for(name) 6 | assets[name] 7 | end 8 | -------------------------------------------------------------------------------- /spec/dummy/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | include Jader::Serialize 3 | jade_serializable :name, :email 4 | end -------------------------------------------------------------------------------- /spec/dummy/app/views/users/show.jade: -------------------------------------------------------------------------------- 1 | h1 My name is #{user.name} 2 | h2 3 | mixin upcase(user.email) 4 | p 5 | mixin downcase(user.name) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | *.log 6 | .rvmrc 7 | *.sqlite 8 | spec/dummy/tmp/ 9 | doc/ 10 | .yardoc 11 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | # Specify your gem's dependencies in jade.gemspec 4 | gemspec 5 | 6 | group :test do 7 | gem 'sqlite3' 8 | end 9 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/views/users/index.jade: -------------------------------------------------------------------------------- 1 | h1#topHeading Hello All Users 2 | h2.secondHeading!= Util.ISODate() 3 | - each user in users 4 | p= user.name -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: sqlite3 3 | database: db/development.sqlite 4 | 5 | test: 6 | adapter: sqlite3 7 | database: db/test.sqlite -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Dummy::Application 5 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Dummy::Application.initialize! 6 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/users_controller.rb: -------------------------------------------------------------------------------- 1 | class UsersController < ApplicationController 2 | 3 | def index 4 | @users = User.where('name != "Jim"') 5 | end 6 | 7 | def show 8 | @user = User.find params[:id] 9 | end 10 | end -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20120816043248_user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Migration 2 | def up 3 | create_table :users do |t| 4 | t.string :name 5 | t.string :email 6 | end 7 | end 8 | 9 | def down 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | gemfile = File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | if File.exist?(gemfile) 5 | ENV['BUNDLE_GEMFILE'] = gemfile 6 | require 'bundler' 7 | Bundler.setup 8 | end 9 | 10 | $:.unshift File.expand_path('../../../../lib', __FILE__) -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | # Add your own tasks in files placed in lib/tasks ending in .rake, 3 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 4 | 5 | require File.expand_path('../config/application', __FILE__) 6 | 7 | Dummy::Application.load_tasks 8 | -------------------------------------------------------------------------------- /spec/dummy/script/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. 3 | 4 | APP_PATH = File.expand_path('../../config/application', __FILE__) 5 | require File.expand_path('../../config/boot', __FILE__) 6 | require 'rails/commands' 7 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag "application", :media => "all" %> 6 | <%= javascript_include_tag "application" %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/jader.rb: -------------------------------------------------------------------------------- 1 | Jader.configure do |config| 2 | config.mixins_path = Rails.root.join('app','assets','javascripts','mixins') 3 | config.views_path = Rails.root.join('app','assets','javascripts','views') 4 | config.includes << IO.read(Rails.root.join('app','assets','javascripts','includes','util.js')) 5 | config.prepend_view_path = true 6 | end -------------------------------------------------------------------------------- /spec/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] 9 | end 10 | 11 | -------------------------------------------------------------------------------- /lib/jader.rb: -------------------------------------------------------------------------------- 1 | require 'jader/source' 2 | require 'jader/compiler' 3 | require 'jader/template' 4 | require 'jader/renderer' 5 | require 'jader/configuration' 6 | require 'jader/serialize' 7 | require 'jader/engine' if defined?(::Rails) 8 | 9 | ActionView::Template.register_template_handler :jade, Jader::Renderer 10 | ActionView::Template.register_template_handler 'jst.jade', Jader::Renderer 11 | 12 | Jader.configure do |config|; end -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/includes/util.js: -------------------------------------------------------------------------------- 1 | // This is a mock Util object 2 | // Used for testing server-side inclusion of Javascripts 3 | ;(function(global){ 4 | 5 | var Util = { 6 | ISODate: function(){ 7 | var d = new Date() 8 | return d.toISOString() 9 | } 10 | }; 11 | // attach to global object to make it available inside jade templates with node.js 12 | global.Util = Util; 13 | 14 | })(this); -------------------------------------------------------------------------------- /spec/support/matchers.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :serve do |asset_name| 2 | match do |sprockets| 3 | !!sprockets[asset_name] 4 | end 5 | 6 | failure_message do |sprockets| 7 | "expected #{asset_name} to be served, but it wasn't" 8 | end 9 | 10 | failure_message_when_negated do |sprockets| 11 | "expected #{asset_name} NOT to be served, but it was" 12 | end 13 | 14 | description do 15 | "serve #{asset_name}" 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Dummy::Application.config.session_store :cookie_store, key: '_dummy_session' 4 | 5 | # Use the database for sessions instead of the cookie-based default, 6 | # which shouldn't be used to store highly confidential information 7 | # (create the session table with "rails generate session_migration") 8 | # Dummy::Application.config.session_store :active_record_store 9 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | Dummy::Application.config.secret_token = '968b4807e6e4a7e64e3dfaf80293c76ac102f443bd0d80c70499451098c256b22e00d843c3ac74e19b2d6bab7d3c438191433bc250091c7644a563c36038ae94' 8 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into including all the files listed below. 2 | // Add new JavaScript/Coffee code in separate files in this directory and they'll automatically 3 | // be included in the compiled file accessible from http://example.com/assets/application.js 4 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 5 | // the compiled file. 6 | // 7 | //= require 'jade/runtime' 8 | //= require ./sample 9 | //= require views/users/dummy 10 | -------------------------------------------------------------------------------- /spec/source_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Jader::Source do 4 | it "should contain jade asset" do 5 | File.exist?(Jader::Source::jade_path).should == true 6 | end 7 | 8 | it "should contain runtime asset" do 9 | File.exist?(Jader::Source::runtime_path).should == true 10 | end 11 | 12 | it "should be able to read jade source" do 13 | IO.read(Jader::Source::jade_path).should_not be_empty 14 | end 15 | 16 | it "should be able to read runtime source" do 17 | IO.read(Jader::Source::runtime_path).should_not be_empty 18 | end 19 | end -------------------------------------------------------------------------------- /spec/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | # 12 | # These inflection rules are supported but not enabled by default: 13 | # ActiveSupport::Inflector.inflections do |inflect| 14 | # inflect.acronym 'RESTful' 15 | # end 16 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the top of the 9 | * compiled file, but it's generally better to create a new file per style scope. 10 | * 11 | *= require_self 12 | *= require_tree . 13 | */ 14 | -------------------------------------------------------------------------------- /lib/jader/source.rb: -------------------------------------------------------------------------------- 1 | module Jader 2 | # Jade template engine Javascript source code 3 | module Source 4 | 5 | # Jade source code 6 | def self.jade 7 | IO.read jade_path 8 | end 9 | 10 | # Jade runtime source code 11 | def self.runtime 12 | IO.read runtime_path 13 | end 14 | 15 | # Jade source code path 16 | def self.jade_path 17 | File.expand_path("../../../vendor/assets/javascripts/jade/jade.js", __FILE__) 18 | end 19 | 20 | # Jade runtime source code path 21 | def self.runtime_path 22 | File.expand_path("../../../vendor/assets/javascripts/jade/runtime.js", __FILE__) 23 | end 24 | end 25 | end -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

We're sorry, but something went wrong.

23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /spec/compiler_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Jader::Compiler do 4 | before :each do 5 | @compiler = Jader::Compiler.new 6 | end 7 | 8 | it "should respect default options" do 9 | @compiler.options.should == { 10 | :client => true, 11 | :compileDebug => false 12 | } 13 | end 14 | 15 | it "should define Jade.JS compiler version" do 16 | @compiler.jade_version.should == "0.27.6" 17 | end 18 | 19 | it "should compile small thing" do 20 | @compiler.compile('test').should include "" 21 | end 22 | 23 | it "should depend on options" do 24 | @compiler.compile('test').should_not include "lineno: 1" 25 | @compiler.options[:compileDebug] = true 26 | @compiler.compile('test').should include "lineno: 1" 27 | end 28 | end -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The change you wanted was rejected.

23 |

Maybe you tried to change something you didn't have access to.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] = "test" 2 | 3 | require 'rubygems' 4 | require 'bundler/setup' 5 | require 'pry' 6 | 7 | require File.expand_path("../dummy/config/environment.rb", __FILE__) 8 | require "rails/test_help" 9 | require "rspec/rails" 10 | require 'rspec/active_model/mocks' 11 | 12 | require 'jader' 13 | 14 | Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f } 15 | 16 | Rails.backtrace_cleaner.remove_silencers! 17 | 18 | RSpec.configure do |config| 19 | require 'rspec/mocks' 20 | require 'rspec/expectations' 21 | config.include RSpec::Matchers 22 | 23 | # Enable old-style rspec API 24 | config.mock_with :rspec do |c| 25 | c.syntax = :should 26 | end 27 | config.expect_with :rspec do |c| 28 | c.syntax = :should 29 | end 30 | config.infer_spec_type_from_file_location! 31 | end -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 17 | 18 | 19 | 20 | 21 |
22 |

The page you were looking for doesn't exist.

23 |

You may have mistyped the address or the page may have moved.

24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/jader/template.rb: -------------------------------------------------------------------------------- 1 | require 'tilt/template' 2 | 3 | module Jader 4 | # Jader Tilt template for use with JST 5 | class Template < Tilt::Template 6 | self.default_mime_type = 'application/javascript' 7 | 8 | def initializer 9 | super 10 | end 11 | 12 | # Ensure ExecJS is available when engine is initialized 13 | def self.engine_initialized? 14 | defined? ::ExecJS 15 | end 16 | 17 | # Require 'execjs' when initializing engine 18 | def initialize_engine 19 | require_template_library 'execjs' 20 | end 21 | 22 | def prepare 23 | end 24 | 25 | # Evaluate the template. Compiles the template for JST 26 | # @return [String] JST-compliant compiled version of the Jade template being rendered 27 | def evaluate(scope, locals, &block) 28 | Jader::Compiler.new(:filename => file).compile(data, file) 29 | end 30 | 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/jader/engine.rb: -------------------------------------------------------------------------------- 1 | require 'sprockets' 2 | require 'sprockets/engines' 3 | 4 | module Jader 5 | class Engine < Rails::Engine 6 | initializer "jade.configure_rails_initialization", :before => 'sprockets.environment', :group => :all do |app| 7 | sprockets_env = app.assets || Sprockets 8 | sprockets_env.register_engine '.jade', ::Jader::Template 9 | end 10 | 11 | initializer 'jader.prepend_views_path', :after => :add_view_paths do |app| 12 | next if Jader::configuration.nil? or Jader::configuration.views_path.nil? 13 | app.config.paths['app/views'].unshift(Jader::configuration.views_path) 14 | if Jader::configuration.prepend_view_path 15 | ActionController::Base.class_eval do 16 | before_filter do |controller| 17 | prepend_view_path Jader::configuration.views_path 18 | end 19 | end 20 | end 21 | end 22 | 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/dummy/db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended to check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(:version => 20120816043248) do 15 | 16 | create_table "users", :force => true do |t| 17 | t.string "name" 18 | t.string "email" 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /lib/jader/configuration.rb: -------------------------------------------------------------------------------- 1 | module Jader 2 | class << self 3 | attr_accessor :configuration 4 | end 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','mixins') 11 | # config.views_path = Rails.root.join('app','assets','javascripts','views') 12 | # config.includes << IO.read Rails.root.join('app','assets','javascripts','util.js') 13 | # end 14 | def self.configure 15 | self.configuration ||= Configuration.new 16 | yield(configuration) 17 | end 18 | 19 | # Jader configuration class 20 | class Configuration 21 | attr_accessor :mixins_path, :views_path, :includes, :prepend_view_path 22 | 23 | # Initialize Jader::Configuration class with default values 24 | def initialize 25 | @mixins_path = nil 26 | @views_path = nil 27 | @includes = [] 28 | @prepend_view_path = false 29 | end 30 | end 31 | end -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2012 Round Lake, inc., 4 | Peter Zotov . 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger 20 | config.active_support.deprecation = :log 21 | 22 | # Only use best-standards-support built into browsers 23 | config.action_dispatch.best_standards_support = :builtin 24 | 25 | 26 | # Do not compress assets 27 | config.assets.compress = false 28 | 29 | # Expands the lines which load the assets 30 | config.assets.debug = true 31 | end 32 | -------------------------------------------------------------------------------- /spec/template_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fileutils' 3 | describe Jader::Compiler do 4 | 5 | before :all do 6 | d = Rails.root.join('tmp','cache','assets') 7 | FileUtils.rm_r d if Dir.exists? d 8 | end 9 | 10 | def template(source, file) 11 | Jader::Template.new(file){source} 12 | end 13 | 14 | it 'should have default mime type' do 15 | Jader::Template.default_mime_type.should == 'application/javascript' 16 | end 17 | 18 | it 'should be served' do 19 | assets.should serve 'sample.js' 20 | asset_for('sample.js').body.should include "Yap, it works" 21 | end 22 | 23 | it 'should work fine with JST' do 24 | context = ExecJS.compile %{ 25 | #{asset_for('application.js').to_s} 26 | html = JST['sample']({name: 'Yorik'}) 27 | } 28 | context.eval('html').should == "Hello, Yorik :)Yap, it works" 29 | end 30 | 31 | it 'should use mixins in JST' do 32 | phrase = 'Hi There' 33 | context = ExecJS.compile %{ 34 | #{asset_for('application.js').to_s} 35 | html = JST['views/users/dummy']({phrase: '#{phrase}'}) 36 | } 37 | context.eval('html').should include(phrase.upcase) 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /jader.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "jader/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "jader" 7 | s.version = Jader::VERSION 8 | s.authors = ["Zohar Arad", "Boris Staal"] 9 | s.email = ["zohar@zohararad.com" "boris@roundlake.ru"] 10 | s.homepage = "https://github.com/zohararad/jader" 11 | s.summary = %q{JST and Rails views compiler for Jade templates} 12 | s.description = %q{Share your Jade views between client and server, eliminate code duplication and make your single-page app SEO friendly} 13 | 14 | s.rubyforge_project = "jader" 15 | 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 18 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 19 | s.require_paths = ["lib"] 20 | 21 | s.add_dependency 'tilt' 22 | s.add_dependency 'sprockets' 23 | s.add_dependency 'execjs' 24 | s.add_development_dependency 'rspec', '~> 3.1' 25 | s.add_development_dependency 'rspec-rails', '~> 3.1' 26 | s.add_development_dependency 'rspec-activemodel-mocks', '~> 1.0' 27 | s.add_development_dependency 'rails', '~> 3.2' 28 | s.add_development_dependency 'pry' 29 | s.add_development_dependency 'yard' 30 | end 31 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Configure static asset server for tests with Cache-Control for performance 11 | config.serve_static_assets = true 12 | config.static_cache_control = "public, max-age=3600" 13 | 14 | # Log error messages when you accidentally call methods on nil 15 | config.whiny_nils = true 16 | 17 | # Show full error reports and disable caching 18 | config.consider_all_requests_local = true 19 | config.action_controller.perform_caching = false 20 | 21 | # Raise exceptions instead of rendering exception templates 22 | config.action_dispatch.show_exceptions = false 23 | 24 | # Disable request forgery protection in test environment 25 | config.action_controller.allow_forgery_protection = false 26 | 27 | # Tell Action Mailer not to deliver emails to the real world. 28 | # The :test delivery method accumulates sent emails in the 29 | # ActionMailer::Base.deliveries array. 30 | config.action_mailer.delivery_method = :test 31 | 32 | 33 | # Print deprecation notices to the stderr 34 | config.active_support.deprecation = :stderr 35 | end 36 | -------------------------------------------------------------------------------- /spec/integration/renderer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Jader::Renderer do 4 | 5 | context 'users/index' do 6 | 7 | before do 8 | User.all.each do |user| 9 | user.destroy 10 | end 11 | @joe = User.create(:name => 'Joe', :email => 'joe@gmail.com') 12 | @mike = User.create(:name => 'Mike', :email => 'mike@gmail.com') 13 | end 14 | 15 | before :each do 16 | get '/users' 17 | end 18 | 19 | it 'render users index page' do 20 | response.should render_template(:index) 21 | end 22 | 23 | it 'renders the template HTML' do 24 | response.body.should include '

Hello All Users

' 25 | end 26 | 27 | it 'renders users' do 28 | response.body.should include @mike.name 29 | response.body.should include @joe.name 30 | end 31 | 32 | it 'uses a function available from server-side includes' do 33 | response.body.should =~ /\d{4}\-\d{2}\-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z/ 34 | end 35 | end 36 | 37 | context 'users/show' do 38 | 39 | before :each do 40 | @user = stub_model(User, :name => 'Sam', :email => 'sam@gmail.com') 41 | User.should_receive(:find).and_return(@user) 42 | end 43 | 44 | it 'renders instance variables' do 45 | get user_path(@user) 46 | response.body.should include 'My name is %s' % @user.name 47 | end 48 | 49 | it 'renders HTML generated by application mixin' do 50 | get user_path(@user) 51 | response.body.should include @user.name.downcase 52 | end 53 | 54 | it 'renders HTML generated by users mixin' do 55 | get user_path(@user) 56 | response.body.should include @user.email.upcase 57 | end 58 | end 59 | end -------------------------------------------------------------------------------- /lib/jader/renderer.rb: -------------------------------------------------------------------------------- 1 | module Jader 2 | # Server side Jade templates renderer 3 | module Renderer 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 11 | def self.convert_template(template_text, controller_name, vars = {}) 12 | compiler = Jader::Compiler.new :client => true 13 | compiler.render(template_text, controller_name, vars) 14 | end 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 20 | def self.call(template) 21 | template.source.gsub!(/\#\{([^\}]+)\}/,"\\\#{\\1}") # escape Jade's #{somevariable} syntax 22 | %{ 23 | template_source = %{#{template.source}} 24 | controller_name = controller.controller_name 25 | variable_names = controller.instance_variable_names 26 | variable_names -= %w[@template] 27 | if controller.respond_to?(:protected_instance_variables) 28 | variable_names -= controller.protected_instance_variables 29 | end 30 | 31 | variables = {} 32 | variable_names.each do |name| 33 | next if name.include? '@_' 34 | variables[name.sub(/^@/, "")] = controller.instance_variable_get(name) 35 | end 36 | Jader::Renderer.convert_template(template_source, controller_name, variables.merge(local_assigns)) 37 | } 38 | end 39 | 40 | end 41 | end -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.routes.draw do 2 | 3 | resources :users, :only => [:index, :show] 4 | # The priority is based upon order of creation: 5 | # first created -> highest priority. 6 | 7 | # Sample of regular route: 8 | # match 'products/:id' => 'catalog#view' 9 | # Keep in mind you can assign values other than :controller and :action 10 | 11 | # Sample of named route: 12 | # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase 13 | # This route can be invoked with purchase_url(:id => product.id) 14 | 15 | # Sample resource route (maps HTTP verbs to controller actions automatically): 16 | # resources :products 17 | 18 | # Sample resource route with options: 19 | # resources :products do 20 | # member do 21 | # get 'short' 22 | # post 'toggle' 23 | # end 24 | # 25 | # collection do 26 | # get 'sold' 27 | # end 28 | # end 29 | 30 | # Sample resource route with sub-resources: 31 | # resources :products do 32 | # resources :comments, :sales 33 | # resource :seller 34 | # end 35 | 36 | # Sample resource route with more complex sub-resources 37 | # resources :products do 38 | # resources :comments 39 | # resources :sales do 40 | # get 'recent', :on => :collection 41 | # end 42 | # end 43 | 44 | # Sample resource route within a namespace: 45 | # namespace :admin do 46 | # # Directs /admin/products/* to Admin::ProductsController 47 | # # (app/controllers/admin/products_controller.rb) 48 | # resources :products 49 | # end 50 | 51 | # You can have the root of your site routed with "root" 52 | # just remember to delete public/index.html. 53 | # root :to => 'welcome#index' 54 | 55 | # See how all your routes lay out with "rake routes" 56 | 57 | # This is a legacy wild controller route that's not recommended for RESTful applications. 58 | # Note: This route will make all actions in every controller accessible via GET requests. 59 | # match ':controller(/:action(/:id))(.:format)' 60 | end 61 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Dummy::Application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb 3 | 4 | # Code is not reloaded between requests 5 | config.cache_classes = true 6 | 7 | # Full error reports are disabled and caching is turned on 8 | config.consider_all_requests_local = false 9 | config.action_controller.perform_caching = true 10 | 11 | # Disable Rails's static asset server (Apache or nginx will already do this) 12 | config.serve_static_assets = false 13 | 14 | # Compress JavaScripts and CSS 15 | config.assets.compress = true 16 | 17 | # Don't fallback to assets pipeline if a precompiled asset is missed 18 | config.assets.compile = false 19 | 20 | # Generate digests for assets URLs 21 | config.assets.digest = true 22 | 23 | # Defaults to nil and saved in location specified by config.assets.prefix 24 | # config.assets.manifest = YOUR_PATH 25 | 26 | # Specifies the header that your server uses for sending files 27 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 28 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 29 | 30 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 31 | # config.force_ssl = true 32 | 33 | # See everything in the log (default is :info) 34 | # config.log_level = :debug 35 | 36 | # Prepend all log lines with the following tags 37 | # config.log_tags = [ :subdomain, :uuid ] 38 | 39 | # Use a different logger for distributed setups 40 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 41 | 42 | # Use a different cache store in production 43 | # config.cache_store = :mem_cache_store 44 | 45 | # Enable serving of images, stylesheets, and JavaScripts from an asset server 46 | # config.action_controller.asset_host = "http://assets.example.com" 47 | 48 | # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) 49 | # config.assets.precompile += %w( search.js ) 50 | 51 | # Disable delivery errors, bad email addresses will be ignored 52 | # config.action_mailer.raise_delivery_errors = false 53 | 54 | # Enable threaded mode 55 | # config.threadsafe! 56 | 57 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 58 | # the I18n.default_locale when a translation can not be found) 59 | config.i18n.fallbacks = true 60 | 61 | # Send deprecation notices to registered listeners 62 | config.active_support.deprecation = :notify 63 | 64 | end 65 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | # Pick the frameworks you want: 4 | require "active_record/railtie" 5 | require "action_controller/railtie" 6 | require "action_mailer/railtie" 7 | require "active_resource/railtie" 8 | require "sprockets/railtie" 9 | # require "rails/test_unit/railtie" 10 | 11 | Bundler.require 12 | require "jader" 13 | 14 | module Dummy 15 | class Application < Rails::Application 16 | # Settings in config/environments/* take precedence over those specified here. 17 | # Application configuration should go into files in config/initializers 18 | # -- all .rb files in that directory are automatically loaded. 19 | 20 | # Custom directories with classes and modules you want to be autoloadable. 21 | # config.autoload_paths += %W(#{config.root}/extras) 22 | 23 | # Only load the plugins named here, in the order given (default is alphabetical). 24 | # :all can be used as a placeholder for all plugins not explicitly named. 25 | # config.plugins = [ :exception_notification, :ssl_requirement, :all ] 26 | 27 | # Activate observers that should always be running. 28 | # config.active_record.observers = :cacher, :garbage_collector, :forum_observer 29 | 30 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 31 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 32 | # config.time_zone = 'Central Time (US & Canada)' 33 | 34 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 35 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 36 | # config.i18n.default_locale = :de 37 | 38 | # Configure the default encoding used in templates for Ruby 1.9. 39 | config.encoding = "utf-8" 40 | 41 | # Configure sensitive parameters which will be filtered from the log file. 42 | config.filter_parameters += [:password] 43 | 44 | # Enable escaping HTML in JSON. 45 | config.active_support.escape_html_entities_in_json = true 46 | 47 | # Use SQL instead of Active Record's schema dumper when creating the database. 48 | # This is necessary if your schema can't be completely dumped by the schema dumper, 49 | # like if you have constraints or database-specific column types 50 | # config.active_record.schema_format = :sql 51 | 52 | # Enforce whitelist mode for mass assignment. 53 | # This will create an empty whitelist of attributes available for mass-assignment for all models 54 | # in your app. As such, your models will need to explicitly whitelist or blacklist accessible 55 | # parameters by using an attr_accessible or attr_protected declaration. 56 | # config.active_record.whitelist_attributes = true 57 | 58 | # Enable the asset pipeline 59 | config.assets.enabled = true 60 | 61 | # Version of your assets, change this if you want to expire all your assets 62 | config.assets.version = '1.0' 63 | end 64 | end 65 | 66 | -------------------------------------------------------------------------------- /lib/jader/serialize.rb: -------------------------------------------------------------------------------- 1 | module Jader 2 | 3 | module Serialize 4 | 5 | module ClassMethods 6 | 7 | # Enable serialization on ActiveModel classes 8 | # @param [Array] 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 16 | def jade_serializable(*args) 17 | serialize = { 18 | :attrs => [], 19 | :merge => true 20 | } 21 | args.each do |arg| 22 | if arg.is_a? Symbol 23 | serialize[:attrs] << arg 24 | elsif arg.is_a? Hash 25 | serialize[:merge] = arg[:merge] if arg.include?(:merge) 26 | end 27 | end 28 | class_variable_set(:@@serialize, serialize) 29 | end 30 | 31 | end 32 | 33 | #nodoc 34 | def self.included(base) 35 | base.extend ClassMethods 36 | end 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 | 45 | ans = h[attr].class.ancestors 46 | if h[attr].class.respond_to?(:jade_serializable) || ans.include?(Enumerable) || ans.include?(ActiveModel::Validations) 47 | h[attr] = h[attr].to_jade 48 | else 49 | end 50 | end 51 | h 52 | end 53 | 54 | # List of Model attributes that should be serialized when called `to_jade` on Model instance 55 | # @return [Array] list of serializable attributes 56 | def jade_attributes 57 | s = self.class.class_variable_get(:@@serialize) 58 | if s[:merge] 59 | attrs = s[:attrs] + self.attributes.keys 60 | else 61 | attrs = s[:attrs] 62 | end 63 | attrs.collect{|attr| attr.to_sym}.uniq 64 | end 65 | end 66 | 67 | end 68 | 69 | class Object 70 | # Serialize Object to Jade format. Invoke `self.to_jade` if instance responds to `to_jade` 71 | def to_jade 72 | if self.respond_to? :to_a 73 | self.to_a.to_jade 74 | else 75 | nil 76 | end 77 | end 78 | end 79 | 80 | #nodoc 81 | [FalseClass, TrueClass, Numeric, String].each do |cls| 82 | cls.class_eval do 83 | def to_jade 84 | self 85 | end 86 | end 87 | end 88 | 89 | class Array 90 | 91 | # Serialize Array to Jade format. Invoke `to_jade` on array members 92 | def to_jade 93 | map {|a| a.respond_to?(:to_jade) ? a.to_jade : a } 94 | end 95 | end 96 | 97 | class Hash 98 | 99 | # Serialize Hash to Jade format. Invoke `to_jade` on members 100 | def to_jade 101 | res = {} 102 | each_pair do |key, value| 103 | res[key] = (value.respond_to?(:to_jade) ? value.to_jade : value) 104 | end 105 | res 106 | end 107 | end -------------------------------------------------------------------------------- /lib/jader/compiler.rb: -------------------------------------------------------------------------------- 1 | require 'execjs' 2 | 3 | module Jader 4 | class Compiler 5 | attr_accessor :options 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`) 11 | def initialize(options={}) 12 | @options = { 13 | :client => true, 14 | :compileDebug => false 15 | }.merge options 16 | end 17 | 18 | # Jade template engine Javascript source code used to compile templates in ExecJS 19 | # @return [String] Jade source code 20 | def source 21 | @source ||= %{ 22 | var window = {}; 23 | #{Jader::Source::jade} 24 | var jade = window.jade; 25 | } 26 | end 27 | 28 | # ExecJS context with Jade code compiled 29 | # @return [ExecJS::Context] compiled Jade source code context 30 | def context 31 | @context ||= ExecJS.compile source 32 | end 33 | 34 | # ExecJS context for the server-side rendering with Jade code compiled 35 | # @return [ExecJS::Context] compiled Jade source code context for the server-side rendering 36 | def server_context 37 | @server_context ||= ExecJS.compile([ 38 | source, 39 | Jader.configuration.includes.join("\n") 40 | ].join(" ")) 41 | end 42 | 43 | # Jade Javascript engine version 44 | # @return [String] version of Jade javascript engine installed in `vendor/assets/javascripts` 45 | def jade_version 46 | context.eval("jade.version") 47 | end 48 | 49 | # Compile a Jade template for client-side use with JST 50 | # @param [String, File] template Jade template file or text to compile 51 | # @param [String] file_name name of template file used to resolve mixins inclusion 52 | # @return [String] Jade template compiled into Javascript and wrapped inside an anonymous function for JST 53 | def compile(template, file_name = '') 54 | template = template.read if template.respond_to?(:read) 55 | file_name.match(/views\/([^\/]+)\//) 56 | controller_name = $1 || nil 57 | context.eval("jade.compile(#{combo(controller_name, template)},#{@options.to_json}).toString()").to_s.sub('function anonymous','function') 58 | end 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 65 | def render(template, controller_name, vars = {}) 66 | server_context.exec("return jade.compile(#{combo(controller_name, template)})(#{vars.to_jade.to_json})") 67 | end 68 | 69 | # Jade template mixins for a given controller 70 | # @param [String] controller_name name of Rails controller rendering a Jade template 71 | # @return [Array] array of Jade mixins to use with a Jade template rendered by a Rails controller 72 | def template_mixins(controller_name) 73 | mixins = [] 74 | unless Jader.configuration.mixins_path.nil? 75 | Dir["#{Jader.configuration.mixins_path}/*.jade"].each do |f| 76 | base_name = File.basename(f) 77 | if base_name == '%s_mixins.jade' % controller_name || base_name == 'application_mixins.jade' 78 | mixins << IO.read(f) 79 | end 80 | end 81 | end 82 | mixins 83 | end 84 | 85 | def combo(controller_name, template) 86 | (template_mixins(controller_name) << template).join("\n").to_json 87 | end 88 | 89 | end 90 | end -------------------------------------------------------------------------------- /vendor/assets/javascripts/jade/runtime.js: -------------------------------------------------------------------------------- 1 | 2 | jade = (function(exports){ 3 | /*! 4 | * Jade - runtime 5 | * Copyright(c) 2010 TJ Holowaychuk 6 | * MIT Licensed 7 | */ 8 | 9 | /** 10 | * Lame Array.isArray() polyfill for now. 11 | */ 12 | 13 | if (!Array.isArray) { 14 | Array.isArray = function(arr){ 15 | return '[object Array]' == Object.prototype.toString.call(arr); 16 | }; 17 | } 18 | 19 | /** 20 | * Lame Object.keys() polyfill for now. 21 | */ 22 | 23 | if (!Object.keys) { 24 | Object.keys = function(obj){ 25 | var arr = []; 26 | for (var key in obj) { 27 | if (obj.hasOwnProperty(key)) { 28 | arr.push(key); 29 | } 30 | } 31 | return arr; 32 | } 33 | } 34 | 35 | /** 36 | * Merge two attribute objects giving precedence 37 | * to values in object `b`. Classes are special-cased 38 | * allowing for arrays and merging/joining appropriately 39 | * resulting in a string. 40 | * 41 | * @param {Object} a 42 | * @param {Object} b 43 | * @return {Object} a 44 | * @api private 45 | */ 46 | 47 | exports.merge = function merge(a, b) { 48 | var ac = a['class']; 49 | var bc = b['class']; 50 | 51 | if (ac || bc) { 52 | ac = ac || []; 53 | bc = bc || []; 54 | if (!Array.isArray(ac)) ac = [ac]; 55 | if (!Array.isArray(bc)) bc = [bc]; 56 | ac = ac.filter(nulls); 57 | bc = bc.filter(nulls); 58 | a['class'] = ac.concat(bc).join(' '); 59 | } 60 | 61 | for (var key in b) { 62 | if (key != 'class') { 63 | a[key] = b[key]; 64 | } 65 | } 66 | 67 | return a; 68 | }; 69 | 70 | /** 71 | * Filter null `val`s. 72 | * 73 | * @param {Mixed} val 74 | * @return {Mixed} 75 | * @api private 76 | */ 77 | 78 | function nulls(val) { 79 | return val != null; 80 | } 81 | 82 | /** 83 | * Render the given attributes object. 84 | * 85 | * @param {Object} obj 86 | * @param {Object} escaped 87 | * @return {String} 88 | * @api private 89 | */ 90 | 91 | exports.attrs = function attrs(obj, escaped){ 92 | var buf = [] 93 | , terse = obj.terse; 94 | 95 | delete obj.terse; 96 | var keys = Object.keys(obj) 97 | , len = keys.length; 98 | 99 | if (len) { 100 | buf.push(''); 101 | for (var i = 0; i < len; ++i) { 102 | var key = keys[i] 103 | , val = obj[key]; 104 | 105 | if ('boolean' == typeof val || null == val) { 106 | if (val) { 107 | terse 108 | ? buf.push(key) 109 | : buf.push(key + '="' + key + '"'); 110 | } 111 | } else if (0 == key.indexOf('data') && 'string' != typeof val) { 112 | buf.push(key + "='" + JSON.stringify(val) + "'"); 113 | } else if ('class' == key && Array.isArray(val)) { 114 | buf.push(key + '="' + exports.escape(val.join(' ')) + '"'); 115 | } else if (escaped && escaped[key]) { 116 | buf.push(key + '="' + exports.escape(val) + '"'); 117 | } else { 118 | buf.push(key + '="' + val + '"'); 119 | } 120 | } 121 | } 122 | 123 | return buf.join(' '); 124 | }; 125 | 126 | /** 127 | * Escape the given string of `html`. 128 | * 129 | * @param {String} html 130 | * @return {String} 131 | * @api private 132 | */ 133 | 134 | exports.escape = function escape(html){ 135 | return String(html) 136 | .replace(/&(?!(\w+|\#\d+);)/g, '&') 137 | .replace(//g, '>') 139 | .replace(/"/g, '"'); 140 | }; 141 | 142 | /** 143 | * Re-throw the given `err` in context to the 144 | * the jade in `filename` at the given `lineno`. 145 | * 146 | * @param {Error} err 147 | * @param {String} filename 148 | * @param {String} lineno 149 | * @api private 150 | */ 151 | 152 | exports.rethrow = function rethrow(err, filename, lineno){ 153 | if (!filename) throw err; 154 | 155 | var context = 3 156 | , str = require('fs').readFileSync(filename, 'utf8') 157 | , lines = str.split('\n') 158 | , start = Math.max(lineno - context, 0) 159 | , end = Math.min(lines.length, lineno + context); 160 | 161 | // Error context 162 | var context = lines.slice(start, end).map(function(line, i){ 163 | var curr = i + start + 1; 164 | return (curr == lineno ? ' > ' : ' ') 165 | + curr 166 | + '| ' 167 | + line; 168 | }).join('\n'); 169 | 170 | // Alter exception message 171 | err.path = filename; 172 | err.message = (filename || 'Jade') + ':' + lineno 173 | + '\n' + context + '\n\n' + err.message; 174 | throw err; 175 | }; 176 | 177 | return exports; 178 | 179 | })({}); 180 | -------------------------------------------------------------------------------- /spec/dummy/README.rdoc: -------------------------------------------------------------------------------- 1 | == Welcome to Rails 2 | 3 | Rails is a web-application framework that includes everything needed to create 4 | database-backed web applications according to the Model-View-Control pattern. 5 | 6 | This pattern splits the view (also called the presentation) into "dumb" 7 | templates that are primarily responsible for inserting pre-built data in between 8 | HTML tags. The model contains the "smart" domain objects (such as Account, 9 | Product, Person, Post) that holds all the business logic and knows how to 10 | persist themselves to a database. The controller handles the incoming requests 11 | (such as Save New Account, Update Product, Show Post) by manipulating the model 12 | and directing data to the view. 13 | 14 | In Rails, the model is handled by what's called an object-relational mapping 15 | layer entitled Active Record. This layer allows you to present the data from 16 | database rows as objects and embellish these data objects with business logic 17 | methods. You can read more about Active Record in 18 | link:files/vendor/rails/activerecord/README.html. 19 | 20 | The controller and view are handled by the Action Pack, which handles both 21 | layers by its two parts: Action View and Action Controller. These two layers 22 | are bundled in a single package due to their heavy interdependence. This is 23 | unlike the relationship between the Active Record and Action Pack that is much 24 | more separate. Each of these packages can be used independently outside of 25 | Rails. You can read more about Action Pack in 26 | link:files/vendor/rails/actionpack/README.html. 27 | 28 | 29 | == Getting Started 30 | 31 | 1. At the command prompt, create a new Rails application: 32 | rails new myapp (where myapp is the application name) 33 | 34 | 2. Change directory to myapp and start the web server: 35 | cd myapp; rails server (run with --help for options) 36 | 37 | 3. Go to http://localhost:3000/ and you'll see: 38 | "Welcome aboard: You're riding Ruby on Rails!" 39 | 40 | 4. Follow the guidelines to start developing your application. You can find 41 | the following resources handy: 42 | 43 | * The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html 44 | * Ruby on Rails Tutorial Book: http://www.railstutorial.org/ 45 | 46 | 47 | == Debugging Rails 48 | 49 | Sometimes your application goes wrong. Fortunately there are a lot of tools that 50 | will help you debug it and get it back on the rails. 51 | 52 | First area to check is the application log files. Have "tail -f" commands 53 | running on the server.log and development.log. Rails will automatically display 54 | debugging and runtime information to these files. Debugging info will also be 55 | shown in the browser on requests from 127.0.0.1. 56 | 57 | You can also log your own messages directly into the log file from your code 58 | using the Ruby logger class from inside your controllers. Example: 59 | 60 | class WeblogController < ActionController::Base 61 | def destroy 62 | @weblog = Weblog.find(params[:id]) 63 | @weblog.destroy 64 | logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!") 65 | end 66 | end 67 | 68 | The result will be a message in your log file along the lines of: 69 | 70 | Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1! 71 | 72 | More information on how to use the logger is at http://www.ruby-doc.org/core/ 73 | 74 | Also, Ruby documentation can be found at http://www.ruby-lang.org/. There are 75 | several books available online as well: 76 | 77 | * Programming Ruby: http://www.ruby-doc.org/docs/ProgrammingRuby/ (Pickaxe) 78 | * Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide) 79 | 80 | These two books will bring you up to speed on the Ruby language and also on 81 | programming in general. 82 | 83 | 84 | == Debugger 85 | 86 | Debugger support is available through the debugger command when you start your 87 | Mongrel or WEBrick server with --debugger. This means that you can break out of 88 | execution at any point in the code, investigate and change the model, and then, 89 | resume execution! You need to install ruby-debug to run the server in debugging 90 | mode. With gems, use sudo gem install ruby-debug. Example: 91 | 92 | class WeblogController < ActionController::Base 93 | def index 94 | @posts = Post.all 95 | debugger 96 | end 97 | end 98 | 99 | So the controller will accept the action, run the first line, then present you 100 | with a IRB prompt in the server window. Here you can do things like: 101 | 102 | >> @posts.inspect 103 | => "[#nil, "body"=>nil, "id"=>"1"}>, 105 | #"Rails", "body"=>"Only ten..", "id"=>"2"}>]" 107 | >> @posts.first.title = "hello from a debugger" 108 | => "hello from a debugger" 109 | 110 | ...and even better, you can examine how your runtime objects actually work: 111 | 112 | >> f = @posts.first 113 | => #nil, "body"=>nil, "id"=>"1"}> 114 | >> f. 115 | Display all 152 possibilities? (y or n) 116 | 117 | Finally, when you're ready to resume execution, you can enter "cont". 118 | 119 | 120 | == Console 121 | 122 | The console is a Ruby shell, which allows you to interact with your 123 | application's domain model. Here you'll have all parts of the application 124 | configured, just like it is when the application is running. You can inspect 125 | domain models, change values, and save to the database. Starting the script 126 | without arguments will launch it in the development environment. 127 | 128 | To start the console, run rails console from the application 129 | directory. 130 | 131 | Options: 132 | 133 | * Passing the -s, --sandbox argument will rollback any modifications 134 | made to the database. 135 | * Passing an environment name as an argument will load the corresponding 136 | environment. Example: rails console production. 137 | 138 | To reload your controllers and models after launching the console run 139 | reload! 140 | 141 | More information about irb can be found at: 142 | link:http://www.rubycentral.org/pickaxe/irb.html 143 | 144 | 145 | == dbconsole 146 | 147 | You can go to the command line of your database directly through rails 148 | dbconsole. You would be connected to the database with the credentials 149 | defined in database.yml. Starting the script without arguments will connect you 150 | to the development database. Passing an argument will connect you to a different 151 | database, like rails dbconsole production. Currently works for MySQL, 152 | PostgreSQL and SQLite 3. 153 | 154 | == Description of Contents 155 | 156 | The default directory structure of a generated Ruby on Rails application: 157 | 158 | |-- app 159 | | |-- assets 160 | | |-- images 161 | | |-- javascripts 162 | | `-- stylesheets 163 | | |-- controllers 164 | | |-- helpers 165 | | |-- mailers 166 | | |-- models 167 | | `-- views 168 | | `-- layouts 169 | |-- config 170 | | |-- environments 171 | | |-- initializers 172 | | `-- locales 173 | |-- db 174 | |-- doc 175 | |-- lib 176 | | `-- tasks 177 | |-- log 178 | |-- public 179 | |-- script 180 | |-- test 181 | | |-- fixtures 182 | | |-- functional 183 | | |-- integration 184 | | |-- performance 185 | | `-- unit 186 | |-- tmp 187 | | |-- cache 188 | | |-- pids 189 | | |-- sessions 190 | | `-- sockets 191 | `-- vendor 192 | |-- assets 193 | `-- stylesheets 194 | `-- plugins 195 | 196 | app 197 | Holds all the code that's specific to this particular application. 198 | 199 | app/assets 200 | Contains subdirectories for images, stylesheets, and JavaScript files. 201 | 202 | app/controllers 203 | Holds controllers that should be named like weblogs_controller.rb for 204 | automated URL mapping. All controllers should descend from 205 | ApplicationController which itself descends from ActionController::Base. 206 | 207 | app/models 208 | Holds models that should be named like post.rb. Models descend from 209 | ActiveRecord::Base by default. 210 | 211 | app/views 212 | Holds the template files for the view that should be named like 213 | weblogs/index.html.erb for the WeblogsController#index action. All views use 214 | eRuby syntax by default. 215 | 216 | app/views/layouts 217 | Holds the template files for layouts to be used with views. This models the 218 | common header/footer method of wrapping views. In your views, define a layout 219 | using the layout :default and create a file named default.html.erb. 220 | Inside default.html.erb, call <% yield %> to render the view using this 221 | layout. 222 | 223 | app/helpers 224 | Holds view helpers that should be named like weblogs_helper.rb. These are 225 | generated for you automatically when using generators for controllers. 226 | Helpers can be used to wrap functionality for your views into methods. 227 | 228 | config 229 | Configuration files for the Rails environment, the routing map, the database, 230 | and other dependencies. 231 | 232 | db 233 | Contains the database schema in schema.rb. db/migrate contains all the 234 | sequence of Migrations for your schema. 235 | 236 | doc 237 | This directory is where your application documentation will be stored when 238 | generated using rake doc:app 239 | 240 | lib 241 | Application specific libraries. Basically, any kind of custom code that 242 | doesn't belong under controllers, models, or helpers. This directory is in 243 | the load path. 244 | 245 | public 246 | The directory available for the web server. Also contains the dispatchers and the 247 | default HTML files. This should be set as the DOCUMENT_ROOT of your web 248 | server. 249 | 250 | script 251 | Helper scripts for automation and generation. 252 | 253 | test 254 | Unit and functional tests along with fixtures. When using the rails generate 255 | command, template test files will be generated for you and placed in this 256 | directory. 257 | 258 | vendor 259 | External libraries that the application depends on. Also includes the plugins 260 | subdirectory. If the app has frozen rails, those gems also go here, under 261 | vendor/rails/. This directory is in the load path. 262 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jader - Share your Jade templates between client and server 2 | 3 | Jade is a very popular templating engine for Node.js. This gem gives 4 | you ability to easily use Jade templates for both server and client side in your Rails project. 5 | 6 | On the client-side your templates should be used with 7 | [Sprockets' JST engine](https://github.com/sstephenson/sprockets#javascript-templating-with-ejs-and-eco). 8 | On the server side, you can render your Jade templates as Rails views (similar to how you'd render ERB or HAML templates). 9 | 10 | ## Writing your templates 11 | 12 | Lets assume you have a users controller `app/controllers/users_controller.rb` which in turn renders a list of users. 13 | We'd like to share that view between the client and the server. 14 | 15 | ### Server-Side code 16 | 17 | ```ruby 18 | class UsersController < ApplicationController 19 | 20 | def index 21 | @users = User.all 22 | respond_to do |format| 23 | format.html 24 | end 25 | end 26 | 27 | end 28 | ``` 29 | 30 | To share our template between client and server, we need to place it under `app/assets/javascripts` for Sprockets' JST engine. 31 | 32 | Lets create a views directory for our shared templates `app/assets/javascripts/views` and add our template there, following Rails' naming convensions. 33 | 34 | The full template path should look like this: `app/assets/javascripts/views/users/index.jst.jade` 35 | 36 | ### Template code 37 | 38 | The most significant differences between using standard server-side Ruby-based engines like ERB or HAML and using Jader are: 39 | 40 | * No access to server-side view helpers (such as url_for) 41 | * No ruby-style instance variable like `@users` 42 | * Template code is Javascript, not Ruby and has to follow Jade's syntax rather than embedded Ruby syntax 43 | * No partials or includes 44 | 45 | Our template code should look like this: 46 | 47 | ```jade 48 | ul.users 49 | each user in users 50 | li.user= user.name 51 | ``` 52 | 53 | Note that rendering this template server-side, will be done inside your application's layout. You can write your views layout file in ERB / HAML 54 | and the call to `=yield` will render your Jade template above. 55 | 56 | ### Sharing template code 57 | 58 | Since Rails doesn't expect server-side templates to live under `app/assets` we need to add our client-side views path to Rails views lookup path. 59 | 60 | Assuming we have an initializer `app/config/initializers/jader.rb` we can add our client-side views directory like this: 61 | 62 | ```ruby 63 | Jader.configure do |config| 64 | # make your client-side views directory discoverable to Rails 65 | config.views_path = Rails.root.join('app','assets','javascripts','views') 66 | end 67 | ``` 68 | 69 | Then, to add a `before_filter` to `ApplicationController::Base` that prepends the provided path to `ActionView::Context` add the following config: 70 | 71 | ```ruby 72 | Jader.configure do |config| 73 | ... 74 | # prepend provided views_path 75 | config.prepend_view_path = true 76 | end 77 | ``` 78 | 79 | ### Client-side code 80 | 81 | To render the same template from the client, we need to fetch our users list from the server and then call our JST template with that list. 82 | 83 | First, lets change our controller to return a JSON formatted list of users when called from the client: 84 | 85 | ```ruby 86 | class UsersController < ApplicationController 87 | 88 | def index 89 | @users = User.all 90 | respond_to do |format| 91 | format.html 92 | format.json { 93 | render :json => @users.to_jade 94 | } 95 | end 96 | end 97 | 98 | end 99 | ``` 100 | 101 | Note the call to `to_jade` on the `@users` collection. This ensures our users are properly serialized for use inside our template. 102 | See the [Serialization](https://github.com/zohararad/jader#serialization) section below for more details. 103 | 104 | In our `application.js` file lets write the following: 105 | 106 | ```javascript 107 | //= require jade/runtime 108 | //= require views/users/index 109 | 110 | $.getJSON('/users', function(users){ 111 | $('body').html(JST['views/users/index']({users:users})); 112 | }); 113 | ``` 114 | 115 | ## Serialization 116 | 117 | To help Jader access Ruby and Rails variables inside the template, we need to employ some sort of JSON serializing before passing 118 | these variables to the template. On the server-side, this happens automagically before the template is rendered. 119 | 120 | Internally, Jader will try to call the `to_jade` method on each instance variable that's passed to the template. Ruby's Hash, Array and Object classes have been extended 121 | to support this functionality. Arrays and Hashes will attempt to call the `to_jade` method on their members when `to_jade` is invoked on their instances. For 122 | other collection-like variables, the `to_jade` method will only be invoked if they respond to a `to_a` method. This allows ActiveModel / ActiveRecord instance variables to 123 | automatically serialize their members before rendering. 124 | 125 | ### Serializing models 126 | 127 | Jader does not assume your Rails models should be serialized by default. Instead, it expects you to enable serializing on desired models explicitly. 128 | 129 | To enable this behaviour, consider the following example: 130 | 131 | ```ruby 132 | class User < ActiveRecord::Base 133 | 134 | include Jader::Serialize 135 | 136 | jade_serializable :name, :email, :favorites, :merge => false 137 | 138 | end 139 | ``` 140 | 141 | The call to `include Jader::Serialize` mixes Jader::Serializer capabilities into our model class. 142 | 143 | We can then tell the serializer which attributes we'd like to serialize, and how we'd like the serialization to work. 144 | 145 | By default, calling `jade_serializable` with no arguments will serialize all your model attributes. Lets look at two examples: 146 | 147 | Consider the following code: 148 | 149 | ```ruby 150 | # define our model 151 | class User < ActiveRecord::Base 152 | 153 | include Jader::Serialize 154 | 155 | jade_serializable 156 | 157 | end 158 | 159 | # access in controller 160 | class UsersController < ApplicationController 161 | 162 | def index 163 | @users = User.all 164 | @users.to_jade # => all available user attributes (users table columns) will be serialized 165 | end 166 | 167 | def active 168 | @users = User.where('active = 1').select('name, email') 169 | @users.to_jade # => only name and email attributes are serialized 170 | end 171 | end 172 | ``` 173 | 174 | For better control over which attributes are serialized, and when serializing model relationships, we can tell the serializer 175 | which attributes should always be serialized, and whether we'd like these attributes to be merged with the default attributes or not. 176 | 177 | Consider the following code: 178 | 179 | 180 | ```ruby 181 | # define our models 182 | 183 | class Favorite < ActiveRecord::Base 184 | 185 | include Jader::Serialize 186 | 187 | jade_serializable 188 | 189 | belongs_to :user 190 | 191 | end 192 | 193 | class User < ActiveRecord::Base 194 | 195 | include Jader::Serialize 196 | 197 | jade_serializable :favorites, :merge => true 198 | 199 | has_many :favorites 200 | end 201 | 202 | # access in controller 203 | class UsersController < ApplicationController 204 | 205 | def active 206 | @users = User.where('active = 1').select('name, email') 207 | @users.to_jade # => only name, email and favorites attributes are serialized 208 | end 209 | end 210 | ``` 211 | 212 | In the above, we defined serialization for the `User` model to include the `:favorites` attribute, which is available because of the `has_many` relationship 213 | to the `Favorite` model. Additionally, we specified that serialization should merge model default attributes with the specified attributes, by setting `:merge => true` . 214 | 215 | This will result in merging `self.attributes` and `self.favorites` on any instance of the `User` model when calling the `to_jade` method on it. 216 | 217 | To only serialize the specified attributes, call `jade_serializable` with `:merge => false` . 218 | 219 | Invokation format for `jade_serializable` is: 220 | 221 | ```ruby 222 | jade_serializable :attr1, :attr2, :attr3 ...., :merge => true/false 223 | ``` 224 | 225 | By default, `jade_serializable` will operate with `:merge => true` and merge instnace attributes with specified attributes. 226 | 227 | ## Mixins 228 | 229 | Jade has built in support for template mixins. Jader allows you to define and share mixins inside your templates. 230 | 231 | Following Rails helpers conventions, your Jade mixins can be defined either as application-level mixins or as controller-level mixins. 232 | 233 | Assuming we have a users controller, we can add mixins by creating a mixins folder under `app/assets/javascripts` and adding: 234 | 235 | * `app/assets/javascripts/helpers/application_mixins.jade` 236 | * `app/assets/javascripts/helpers/users_mixins.jade` 237 | 238 | Mixins that are defined inside `application_mixins.jade` will be available in all templates. This is a perfect place for example to add a pagination mixin. 239 | 240 | Controller-level mixins are defined inside `CONTROLLER_NAME_mixins.jade` and are available only inside the relevant controller. Since the 241 | client-side has no notion of which controller is currently being rendered, we need to strictly follow Rails naming conventions as follows: 242 | 243 | * Client-side views subdirectories are named after their controller. For example, for `UsersController` we will have `app/assets/javascripts/views/users` 244 | * Client-side Jade mixins files are resolved based on view name. `mixins/users_mixins.jade` will be available for views inside `views/users` folder 245 | 246 | To enable Jader's mixins capabilities we need to configure Jader and tell it where to look for mixins files: 247 | 248 | ```ruby 249 | Jader.configure do |config| 250 | config.mixins_path = Rails.root.join('app','assets','javascripts','mixins') 251 | end 252 | ``` 253 | 254 | ### Notes on performance 255 | 256 | On the client-side, Jader will add the mixins code into your Jade JST template. This can potentially increase your client-side file size dramatically. Since application-level 257 | mixins are included in each and every template, please be sure to keep them to a bare minimum. 258 | 259 | ## Javascript inclusion 260 | 261 | Often we'll have additional Javascript that is included client-side and adds more functionality to our client-side application. Two such examples could be 262 | using `I18n.js` for internationalization, or `Date.js` for better date handling. 263 | 264 | Since our server-side templates cannot access this code, we need to figure out a way to share arbitrary Javascript from our client in the server. 265 | 266 | This is where inclusions come in. 267 | 268 | We can tell Jader to add arbitrary pieces of raw Javascript to the server-side rendering context before evaluating our template like so: 269 | 270 | ```ruby 271 | Jader.configure do |config| 272 | config.includes << IO.read(Rails.root.join('app','assets','javascripts','includes','util.js')) 273 | end 274 | ``` 275 | 276 | `Jader.configuration.includes` is an array that accepts raw Javascript strings that are, in turn, passed to the server-side template evaluation context. 277 | 278 | To give a more pragmatic example of using Jader inclusions, lets try using `I18n.js` on both server and client. 279 | 280 | For the sake of this example, we assume `gem 'i18n-js'` is installed in our application. 281 | 282 | Our `application.js` file will then include: 283 | 284 | ``` javascript 285 | //= require i18n 286 | //= require i18n/translations 287 | ``` 288 | 289 | The first require is made available via `i18n-js` vendorized assets while the second require is a translations file inside our `app/assets/javascripts/i18n` folder. 290 | 291 | To enable I18n support when rendering templates on the server, we configure Jader as follows: 292 | 293 | ```ruby 294 | Jader.configure do |config| 295 | # wait for assets to be ready 296 | Rails.application.config.after_initialize do 297 | # inject assets source into Jader's includes array 298 | config.includes << Rails.application.assets['i18n'].source 299 | config.includes << Rails.application.assets['i18n/translations'].source 300 | config.includes << "I18n.defaultLocale = 'en'; I18n.locale = 'en';" 301 | end 302 | end 303 | ``` 304 | 305 | > If you want to use node.js runtime, you'll have to append your javasript modules into the global `this` object (e.g. `this.Util = Util`) to make them visible inside the templates. (See: `/spec/dummy/app/assets/javascripts/includes/util.js`) 306 | 307 | ## Configuration 308 | 309 | Its recommended to configure Jader inside a Rails initializer so that configuration is defined at boot time. 310 | 311 | Assuming we have an initializer `app/config/initializers/jader.rb` it should include: 312 | 313 | ```ruby 314 | Jader.configure do |config| 315 | config.mixins_path = Rails.root.join('app','assets','javascripts','mixins') 316 | # make your client-side views directory discoverable to Rails 317 | config.views_path = Rails.root.join('app','assets','javascripts','views') 318 | # Use some javascript from a file that's not available in the asset pipeline 319 | config.includes << IO.read(Rails.root.join('app','assets','javascripts','includes','util.js')) 320 | # In Rails 3 applications, prepend views_path using a controller before filter. 321 | # This is set to false by default 322 | config.prepend_view_path = true 323 | # wait for assets to be ready 324 | Rails.application.config.after_initialize do 325 | # include javascripts available only from asset pipeline 326 | config.includes << Rails.application.assets['util'].source 327 | end 328 | end 329 | ``` 330 | 331 | ## Asset Pipeline 332 | 333 | In case your Rails asset pipeline is configured **not** to load the entire Rails environment when calling `rake assets:precompile`, you should include Jader's configuration initalizer in your Rakefile. 334 | 335 | Simply add `require File.expand_path('../config/initializers/jader', __FILE__)` before `require File.expand_path('../config/application', __FILE__)` in your Rakefile, and ensure Jader is properly configured when your assets are precompiled 336 | 337 | ## Kudos 338 | 339 | Jader is built upon the wonderful work of [Boris Staal](https://github.com/roundlake/jade/) and draws heavily from: 340 | 341 | - [ice](https://github.com/ludicast/ice) 342 | - [ruby-haml-js](https://github.com/dnagir/ruby-haml-js) 343 | - [tilt-jade](https://github.com/therabidbanana/tilt-jade) 344 | 345 | Boris Staal's [Jade](https://github.com/roundlake/jade/) Rubygem was developed as a successor to tilt-jade to improve following: 346 | 347 | * Add debugging capabilities (slightly different build technique) 348 | * Support exact Jade.JS lib without modifications 349 | * Do not hold 3 copies of Jade.JS internally 350 | * Be well-covered with RSpec 351 | 352 | ## Credits 353 | 354 | 355 | 356 | * Boris Staal ([@_inossidabile](http://twitter.com/#!/_inossidabile)) 357 | 358 | # License 359 | 360 | Copyright (c) 2012 Zohar Arad 361 | 362 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 363 | 364 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 365 | 366 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 367 | -------------------------------------------------------------------------------- /vendor/assets/javascripts/jade/jade.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | // CommonJS require() 4 | 5 | function require(p){ 6 | var path = require.resolve(p) 7 | , mod = require.modules[path]; 8 | if (!mod) throw new Error('failed to require "' + p + '"'); 9 | if (!mod.exports) { 10 | mod.exports = {}; 11 | mod.call(mod.exports, mod, mod.exports, require.relative(path)); 12 | } 13 | return mod.exports; 14 | } 15 | 16 | require.modules = {}; 17 | 18 | require.resolve = function (path){ 19 | var orig = path 20 | , reg = path + '.js' 21 | , index = path + '/index.js'; 22 | return require.modules[reg] && reg 23 | || require.modules[index] && index 24 | || orig; 25 | }; 26 | 27 | require.register = function (path, fn){ 28 | require.modules[path] = fn; 29 | }; 30 | 31 | require.relative = function (parent) { 32 | return function(p){ 33 | if ('.' != p.charAt(0)) return require(p); 34 | 35 | var path = parent.split('/') 36 | , segs = p.split('/'); 37 | path.pop(); 38 | 39 | for (var i = 0; i < segs.length; i++) { 40 | var seg = segs[i]; 41 | if ('..' == seg) path.pop(); 42 | else if ('.' != seg) path.push(seg); 43 | } 44 | 45 | return require(path.join('/')); 46 | }; 47 | }; 48 | 49 | 50 | require.register("compiler.js", function(module, exports, require){ 51 | 52 | /*! 53 | * Jade - Compiler 54 | * Copyright(c) 2010 TJ Holowaychuk 55 | * MIT Licensed 56 | */ 57 | 58 | /** 59 | * Module dependencies. 60 | */ 61 | 62 | var nodes = require('./nodes') 63 | , filters = require('./filters') 64 | , doctypes = require('./doctypes') 65 | , selfClosing = require('./self-closing') 66 | , runtime = require('./runtime') 67 | , utils = require('./utils'); 68 | 69 | 70 | if (!Object.keys) { 71 | Object.keys = function(obj){ 72 | var arr = []; 73 | for (var key in obj) { 74 | if (obj.hasOwnProperty(key)) { 75 | arr.push(key); 76 | } 77 | } 78 | return arr; 79 | } 80 | } 81 | 82 | if (!String.prototype.trimLeft) { 83 | String.prototype.trimLeft = function(){ 84 | return this.replace(/^\s+/, ''); 85 | } 86 | } 87 | 88 | 89 | 90 | /** 91 | * Initialize `Compiler` with the given `node`. 92 | * 93 | * @param {Node} node 94 | * @param {Object} options 95 | * @api public 96 | */ 97 | 98 | var Compiler = module.exports = function Compiler(node, options) { 99 | this.options = options = options || {}; 100 | this.node = node; 101 | this.hasCompiledDoctype = false; 102 | this.hasCompiledTag = false; 103 | this.pp = options.pretty || false; 104 | this.debug = false !== options.compileDebug; 105 | this.indents = 0; 106 | this.parentIndents = 0; 107 | if (options.doctype) this.setDoctype(options.doctype); 108 | }; 109 | 110 | /** 111 | * Compiler prototype. 112 | */ 113 | 114 | Compiler.prototype = { 115 | 116 | /** 117 | * Compile parse tree to JavaScript. 118 | * 119 | * @api public 120 | */ 121 | 122 | compile: function(){ 123 | this.buf = ['var interp;']; 124 | if (this.pp) this.buf.push("var __indent = [];"); 125 | this.lastBufferedIdx = -1; 126 | this.visit(this.node); 127 | return this.buf.join('\n'); 128 | }, 129 | 130 | /** 131 | * Sets the default doctype `name`. Sets terse mode to `true` when 132 | * html 5 is used, causing self-closing tags to end with ">" vs "/>", 133 | * and boolean attributes are not mirrored. 134 | * 135 | * @param {string} name 136 | * @api public 137 | */ 138 | 139 | setDoctype: function(name){ 140 | name = (name && name.toLowerCase()) || 'default'; 141 | this.doctype = doctypes[name] || ''; 142 | this.terse = this.doctype.toLowerCase() == ''; 143 | this.xml = 0 == this.doctype.indexOf(' 1 && !escape && block.nodes[0].isText && block.nodes[1].isText) 294 | this.prettyIndent(1, true); 295 | 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 | 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'); 305 | } 306 | }, 307 | 308 | /** 309 | * Visit `doctype`. Sets terse mode to `true` when html 5 310 | * is used, causing self-closing tags to end with ">" vs "/>", 311 | * and boolean attributes are not mirrored. 312 | * 313 | * @param {Doctype} doctype 314 | * @api public 315 | */ 316 | 317 | visitDoctype: function(doctype){ 318 | if (doctype && (doctype.val || !this.doctype)) { 319 | this.setDoctype(doctype.val || 'default'); 320 | } 321 | 322 | if (this.doctype) this.buffer(this.doctype); 323 | this.hasCompiledDoctype = true; 324 | }, 325 | 326 | /** 327 | * Visit `mixin`, generating a function that 328 | * may be called within the template. 329 | * 330 | * @param {Mixin} mixin 331 | * @api public 332 | */ 333 | 334 | visitMixin: function(mixin){ 335 | var name = mixin.name.replace(/-/g, '_') + '_mixin' 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 | } 364 | 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();") 385 | } else { 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('};'); 392 | } 393 | }, 394 | 395 | /** 396 | * Visit `tag` buffering tag markup, generating 397 | * attributes, visiting the `tag`'s code and block. 398 | * 399 | * @param {Tag} tag 400 | * @api public 401 | */ 402 | 403 | visitTag: function(tag){ 404 | this.indents++; 405 | var name = tag.name 406 | , pp = this.pp; 407 | 408 | if (tag.buffer) name = "' + (" + name + ") + '"; 409 | 410 | if (!this.hasCompiledTag) { 411 | if (!this.hasCompiledDoctype && 'html' == name) { 412 | this.visitDoctype(); 413 | } 414 | this.hasCompiledTag = true; 415 | } 416 | 417 | // pretty print 418 | if (pp && !tag.isInline()) 419 | this.prettyIndent(0, true); 420 | 421 | if ((~selfClosing.indexOf(name) || tag.selfClosing) && !this.xml) { 422 | this.buffer('<' + name); 423 | this.visitAttributes(tag.attrs); 424 | this.terse 425 | ? this.buffer('>') 426 | : this.buffer('/>'); 427 | } else { 428 | // Optimize attributes buffering 429 | if (tag.attrs.length) { 430 | this.buffer('<' + name); 431 | if (tag.attrs.length) this.visitAttributes(tag.attrs); 432 | this.buffer('>'); 433 | } else { 434 | this.buffer('<' + name + '>'); 435 | } 436 | if (tag.code) this.visitCode(tag.code); 437 | this.escape = 'pre' == tag.name; 438 | this.visit(tag.block); 439 | 440 | // pretty print 441 | if (pp && !tag.isInline() && 'pre' != tag.name && !tag.canInline()) 442 | this.prettyIndent(0, true); 443 | 444 | this.buffer(''); 445 | } 446 | this.indents--; 447 | }, 448 | 449 | /** 450 | * Visit `filter`, throwing when the filter does not exist. 451 | * 452 | * @param {Filter} filter 453 | * @api public 454 | */ 455 | 456 | visitFilter: function(filter){ 457 | var fn = filters[filter.name]; 458 | 459 | // unknown filter 460 | if (!fn) { 461 | if (filter.isASTFilter) { 462 | throw new Error('unknown ast filter "' + filter.name + ':"'); 463 | } else { 464 | throw new Error('unknown filter ":' + filter.name + '"'); 465 | } 466 | } 467 | 468 | if (filter.isASTFilter) { 469 | this.buf.push(fn(filter.block, this, filter.attrs)); 470 | } else { 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; 474 | this.buffer(utils.text(fn(text, filter.attrs))); 475 | } 476 | }, 477 | 478 | /** 479 | * Visit `text` node. 480 | * 481 | * @param {Text} text 482 | * @api public 483 | */ 484 | 485 | visitText: function(text){ 486 | text = utils.text(text.val.replace(/\\/g, '_SLASH_')); 487 | if (this.escape) text = escape(text); 488 | text = text.replace(/_SLASH_/g, '\\\\'); 489 | this.buffer(text); 490 | }, 491 | 492 | /** 493 | * Visit a `comment`, only buffering when the buffer flag is set. 494 | * 495 | * @param {Comment} comment 496 | * @api public 497 | */ 498 | 499 | visitComment: function(comment){ 500 | if (!comment.buffer) return; 501 | if (this.pp) this.prettyIndent(1, true); 502 | this.buffer(''); 503 | }, 504 | 505 | /** 506 | * Visit a `BlockComment`. 507 | * 508 | * @param {Comment} comment 509 | * @api public 510 | */ 511 | 512 | visitBlockComment: function(comment){ 513 | if (!comment.buffer) return; 514 | if (0 == comment.val.trim().indexOf('if')) { 515 | this.buffer(''); 518 | } else { 519 | this.buffer(''); 522 | } 523 | }, 524 | 525 | /** 526 | * Visit `code`, respecting buffer / escape flags. 527 | * If the code is followed by a block, wrap it in 528 | * a self-calling function. 529 | * 530 | * @param {Code} code 531 | * @api public 532 | */ 533 | 534 | visitCode: function(code){ 535 | // Wrap code blocks with {}. 536 | // we only wrap unbuffered code blocks ATM 537 | // since they are usually flow control 538 | 539 | // Buffer code 540 | if (code.buffer) { 541 | var val = code.val.trimLeft(); 542 | this.buf.push('var __val__ = ' + val); 543 | val = 'null == __val__ ? "" : __val__'; 544 | if (code.escape) val = 'escape(' + val + ')'; 545 | this.buf.push("buf.push(" + val + ");"); 546 | } else { 547 | this.buf.push(code.val); 548 | } 549 | 550 | // Block support 551 | if (code.block) { 552 | if (!code.buffer) this.buf.push('{'); 553 | this.visit(code.block); 554 | if (!code.buffer) this.buf.push('}'); 555 | } 556 | }, 557 | 558 | /** 559 | * Visit `each` block. 560 | * 561 | * @param {Each} each 562 | * @api public 563 | */ 564 | 565 | visitEach: function(each){ 566 | this.buf.push('' 567 | + '// iterate ' + each.obj + '\n' 568 | + ';(function(){\n' 569 | + ' if (\'number\' == typeof ' + each.obj + '.length) {\n'); 570 | 571 | if (each.alternative) { 572 | this.buf.push(' if (' + each.obj + '.length) {'); 573 | } 574 | 575 | this.buf.push('' 576 | + ' for (var ' + each.key + ' = 0, $$l = ' + each.obj + '.length; ' + each.key + ' < $$l; ' + each.key + '++) {\n' 577 | + ' var ' + each.val + ' = ' + each.obj + '[' + each.key + '];\n'); 578 | 579 | this.visit(each.block); 580 | 581 | this.buf.push(' }\n'); 582 | 583 | if (each.alternative) { 584 | this.buf.push(' } else {'); 585 | this.visit(each.alternative); 586 | this.buf.push(' }'); 587 | } 588 | 589 | this.buf.push('' 590 | + ' } else {\n' 591 | + ' var $$l = 0;\n' 592 | + ' for (var ' + each.key + ' in ' + each.obj + ') {\n' 593 | + ' $$l++;' 594 | + ' if (' + each.obj + '.hasOwnProperty(' + each.key + ')){' 595 | + ' var ' + each.val + ' = ' + each.obj + '[' + each.key + '];\n'); 596 | 597 | this.visit(each.block); 598 | 599 | this.buf.push(' }\n'); 600 | 601 | this.buf.push(' }\n'); 602 | if (each.alternative) { 603 | this.buf.push(' if ($$l === 0) {'); 604 | this.visit(each.alternative); 605 | this.buf.push(' }'); 606 | } 607 | this.buf.push(' }\n}).call(this);\n'); 608 | }, 609 | 610 | /** 611 | * Visit `attrs`. 612 | * 613 | * @param {Array} attrs 614 | * @api public 615 | */ 616 | 617 | visitAttributes: function(attrs){ 618 | var val = this.attrs(attrs); 619 | if (val.inherits) { 620 | this.buf.push("buf.push(attrs(merge({ " + val.buf + 621 | " }, attributes), merge(" + val.escaped + ", escaped, true)));"); 622 | } else if (val.constant) { 623 | eval('var buf={' + val.buf + '};'); 624 | this.buffer(runtime.attrs(buf, JSON.parse(val.escaped)), true); 625 | } else { 626 | this.buf.push("buf.push(attrs({ " + val.buf + " }, " + val.escaped + "));"); 627 | } 628 | }, 629 | 630 | /** 631 | * Compile attributes. 632 | */ 633 | 634 | attrs: function(attrs){ 635 | var buf = [] 636 | , classes = [] 637 | , escaped = {} 638 | , constant = attrs.every(function(attr){ return isConstant(attr.val) }) 639 | , inherits = false; 640 | 641 | if (this.terse) buf.push('terse: true'); 642 | 643 | attrs.forEach(function(attr){ 644 | if (attr.name == 'attributes') return inherits = true; 645 | escaped[attr.name] = attr.escaped; 646 | if (attr.name == 'class') { 647 | classes.push('(' + attr.val + ')'); 648 | } else { 649 | var pair = "'" + attr.name + "':(" + attr.val + ')'; 650 | buf.push(pair); 651 | } 652 | }); 653 | 654 | if (classes.length) { 655 | classes = classes.join(" + ' ' + "); 656 | buf.push("class: " + classes); 657 | } 658 | 659 | return { 660 | buf: buf.join(', ').replace('class:', '"class":'), 661 | escaped: JSON.stringify(escaped), 662 | inherits: inherits, 663 | constant: constant 664 | }; 665 | } 666 | }; 667 | 668 | /** 669 | * Check if expression can be evaluated to a constant 670 | * 671 | * @param {String} expression 672 | * @return {Boolean} 673 | * @api private 674 | */ 675 | 676 | function isConstant(val){ 677 | // Check strings/literals 678 | if (/^ *("([^"\\]*(\\.[^"\\]*)*)"|'([^'\\]*(\\.[^'\\]*)*)'|true|false|null|undefined) *$/i.test(val)) 679 | return true; 680 | 681 | // Check numbers 682 | if (!isNaN(Number(val))) 683 | return true; 684 | 685 | // Check arrays 686 | var matches; 687 | if (matches = /^ *\[(.*)\] *$/.exec(val)) 688 | return matches[1].split(',').every(isConstant); 689 | 690 | return false; 691 | } 692 | 693 | /** 694 | * Escape the given string of `html`. 695 | * 696 | * @param {String} html 697 | * @return {String} 698 | * @api private 699 | */ 700 | 701 | function escape(html){ 702 | return String(html) 703 | .replace(/&(?!\w+;)/g, '&') 704 | .replace(//g, '>') 706 | .replace(/"/g, '"'); 707 | } 708 | 709 | }); // module: compiler.js 710 | 711 | require.register("doctypes.js", function(module, exports, require){ 712 | 713 | /*! 714 | * Jade - doctypes 715 | * Copyright(c) 2010 TJ Holowaychuk 716 | * MIT Licensed 717 | */ 718 | 719 | module.exports = { 720 | '5': '' 721 | , 'default': '' 722 | , 'xml': '' 723 | , 'transitional': '' 724 | , 'strict': '' 725 | , 'frameset': '' 726 | , '1.1': '' 727 | , 'basic': '' 728 | , 'mobile': '' 729 | }; 730 | }); // module: doctypes.js 731 | 732 | require.register("filters.js", function(module, exports, require){ 733 | 734 | /*! 735 | * Jade - filters 736 | * Copyright(c) 2010 TJ Holowaychuk 737 | * MIT Licensed 738 | */ 739 | 740 | module.exports = { 741 | 742 | /** 743 | * Wrap text with CDATA block. 744 | */ 745 | 746 | cdata: function(str){ 747 | return ''; 748 | }, 749 | 750 | /** 751 | * Transform sass to css, wrapped in style tags. 752 | */ 753 | 754 | sass: function(str){ 755 | str = str.replace(/\\n/g, '\n'); 756 | var sass = require('sass').render(str).replace(/\n/g, '\\n'); 757 | return ''; 758 | }, 759 | 760 | /** 761 | * Transform stylus to css, wrapped in style tags. 762 | */ 763 | 764 | stylus: function(str, options){ 765 | var ret; 766 | str = str.replace(/\\n/g, '\n'); 767 | var stylus = require('stylus'); 768 | stylus(str, options).render(function(err, css){ 769 | if (err) throw err; 770 | ret = css.replace(/\n/g, '\\n'); 771 | }); 772 | return ''; 773 | }, 774 | 775 | /** 776 | * Transform less to css, wrapped in style tags. 777 | */ 778 | 779 | less: function(str){ 780 | var ret; 781 | str = str.replace(/\\n/g, '\n'); 782 | require('less').render(str, function(err, css){ 783 | if (err) throw err; 784 | ret = ''; 785 | }); 786 | return ret; 787 | }, 788 | 789 | /** 790 | * Transform markdown to html. 791 | */ 792 | 793 | markdown: function(str){ 794 | var md; 795 | 796 | // support markdown / discount 797 | try { 798 | md = require('markdown'); 799 | } catch (err){ 800 | try { 801 | md = require('discount'); 802 | } catch (err) { 803 | try { 804 | md = require('markdown-js'); 805 | } catch (err) { 806 | try { 807 | md = require('marked'); 808 | } catch (err) { 809 | throw new 810 | Error('Cannot find markdown library, install markdown, discount, or marked.'); 811 | } 812 | } 813 | } 814 | } 815 | 816 | str = str.replace(/\\n/g, '\n'); 817 | return md.parse(str).replace(/\n/g, '\\n').replace(/'/g,'''); 818 | }, 819 | 820 | /** 821 | * Transform coffeescript to javascript. 822 | */ 823 | 824 | coffeescript: function(str){ 825 | var js = require('coffee-script').compile(str).replace(/\\/g, '\\\\').replace(/\n/g, '\\n'); 826 | return ''; 827 | } 828 | }; 829 | 830 | }); // module: filters.js 831 | 832 | require.register("inline-tags.js", function(module, exports, require){ 833 | 834 | /*! 835 | * Jade - inline tags 836 | * Copyright(c) 2010 TJ Holowaychuk 837 | * MIT Licensed 838 | */ 839 | 840 | module.exports = [ 841 | 'a' 842 | , 'abbr' 843 | , 'acronym' 844 | , 'b' 845 | , 'br' 846 | , 'code' 847 | , 'em' 848 | , 'font' 849 | , 'i' 850 | , 'img' 851 | , 'ins' 852 | , 'kbd' 853 | , 'map' 854 | , 'samp' 855 | , 'small' 856 | , 'span' 857 | , 'strong' 858 | , 'sub' 859 | , 'sup' 860 | ]; 861 | }); // module: inline-tags.js 862 | 863 | require.register("jade.js", function(module, exports, require){ 864 | /*! 865 | * Jade 866 | * Copyright(c) 2010 TJ Holowaychuk 867 | * MIT Licensed 868 | */ 869 | 870 | /** 871 | * Module dependencies. 872 | */ 873 | 874 | var Parser = require('./parser') 875 | , Lexer = require('./lexer') 876 | , Compiler = require('./compiler') 877 | , runtime = require('./runtime') 878 | 879 | /** 880 | * Library version. 881 | */ 882 | 883 | exports.version = '0.27.6'; 884 | 885 | /** 886 | * Expose self closing tags. 887 | */ 888 | 889 | exports.selfClosing = require('./self-closing'); 890 | 891 | /** 892 | * Default supported doctypes. 893 | */ 894 | 895 | exports.doctypes = require('./doctypes'); 896 | 897 | /** 898 | * Text filters. 899 | */ 900 | 901 | exports.filters = require('./filters'); 902 | 903 | /** 904 | * Utilities. 905 | */ 906 | 907 | exports.utils = require('./utils'); 908 | 909 | /** 910 | * Expose `Compiler`. 911 | */ 912 | 913 | exports.Compiler = Compiler; 914 | 915 | /** 916 | * Expose `Parser`. 917 | */ 918 | 919 | exports.Parser = Parser; 920 | 921 | /** 922 | * Expose `Lexer`. 923 | */ 924 | 925 | exports.Lexer = Lexer; 926 | 927 | /** 928 | * Nodes. 929 | */ 930 | 931 | exports.nodes = require('./nodes'); 932 | 933 | /** 934 | * Jade runtime helpers. 935 | */ 936 | 937 | exports.runtime = runtime; 938 | 939 | /** 940 | * Template function cache. 941 | */ 942 | 943 | exports.cache = {}; 944 | 945 | /** 946 | * Parse the given `str` of jade and return a function body. 947 | * 948 | * @param {String} str 949 | * @param {Object} options 950 | * @return {String} 951 | * @api private 952 | */ 953 | 954 | function parse(str, options){ 955 | try { 956 | // Parse 957 | var parser = new Parser(str, options.filename, options); 958 | 959 | // Compile 960 | var compiler = new (options.compiler || Compiler)(parser.parse(), options) 961 | , js = compiler.compile(); 962 | 963 | // Debug compiler 964 | if (options.debug) { 965 | console.error('\nCompiled Function:\n\n\033[90m%s\033[0m', js.replace(/^/gm, ' ')); 966 | } 967 | 968 | return '' 969 | + 'var buf = [];\n' 970 | + (options.self 971 | ? 'var self = locals || {};\n' + js 972 | : 'with (locals || {}) {\n' + js + '\n}\n') 973 | + 'return buf.join("");'; 974 | } catch (err) { 975 | parser = parser.context(); 976 | runtime.rethrow(err, parser.filename, parser.lexer.lineno); 977 | } 978 | } 979 | 980 | /** 981 | * Strip any UTF-8 BOM off of the start of `str`, if it exists. 982 | * 983 | * @param {String} str 984 | * @return {String} 985 | * @api private 986 | */ 987 | 988 | function stripBOM(str){ 989 | return 0xFEFF == str.charCodeAt(0) 990 | ? str.substring(1) 991 | : str; 992 | } 993 | 994 | /** 995 | * Compile a `Function` representation of the given jade `str`. 996 | * 997 | * Options: 998 | * 999 | * - `compileDebug` when `false` debugging code is stripped from the compiled template 1000 | * - `client` when `true` the helper functions `escape()` etc will reference `jade.escape()` 1001 | * for use with the Jade client-side runtime.js 1002 | * 1003 | * @param {String} str 1004 | * @param {Options} options 1005 | * @return {Function} 1006 | * @api public 1007 | */ 1008 | 1009 | exports.compile = function(str, options){ 1010 | var options = options || {} 1011 | , client = options.client 1012 | , filename = options.filename 1013 | ? JSON.stringify(options.filename) 1014 | : 'undefined' 1015 | , fn; 1016 | 1017 | str = stripBOM(String(str)); 1018 | 1019 | if (options.compileDebug !== false) { 1020 | fn = [ 1021 | 'var __jade = [{ lineno: 1, filename: ' + filename + ' }];' 1022 | , 'try {' 1023 | , parse(str, options) 1024 | , '} catch (err) {' 1025 | , ' rethrow(err, __jade[0].filename, __jade[0].lineno);' 1026 | , '}' 1027 | ].join('\n'); 1028 | } else { 1029 | fn = parse(str, options); 1030 | } 1031 | 1032 | if (client) { 1033 | fn = 'attrs = attrs || jade.attrs; escape = escape || jade.escape; rethrow = rethrow || jade.rethrow; merge = merge || jade.merge;\n' + fn; 1034 | } 1035 | 1036 | fn = new Function('locals, attrs, escape, rethrow, merge', fn); 1037 | 1038 | if (client) return fn; 1039 | 1040 | return function(locals){ 1041 | return fn(locals, runtime.attrs, runtime.escape, runtime.rethrow, runtime.merge); 1042 | }; 1043 | }; 1044 | 1045 | /** 1046 | * Render the given `str` of jade and invoke 1047 | * the callback `fn(err, str)`. 1048 | * 1049 | * Options: 1050 | * 1051 | * - `cache` enable template caching 1052 | * - `filename` filename required for `include` / `extends` and caching 1053 | * 1054 | * @param {String} str 1055 | * @param {Object|Function} options or fn 1056 | * @param {Function} fn 1057 | * @api public 1058 | */ 1059 | 1060 | exports.render = function(str, options, fn){ 1061 | // swap args 1062 | if ('function' == typeof options) { 1063 | fn = options, options = {}; 1064 | } 1065 | 1066 | // cache requires .filename 1067 | if (options.cache && !options.filename) { 1068 | return fn(new Error('the "filename" option is required for caching')); 1069 | } 1070 | 1071 | try { 1072 | var path = options.filename; 1073 | var tmpl = options.cache 1074 | ? exports.cache[path] || (exports.cache[path] = exports.compile(str, options)) 1075 | : exports.compile(str, options); 1076 | fn(null, tmpl(options)); 1077 | } catch (err) { 1078 | fn(err); 1079 | } 1080 | }; 1081 | 1082 | /** 1083 | * Render a Jade file at the given `path` and callback `fn(err, str)`. 1084 | * 1085 | * @param {String} path 1086 | * @param {Object|Function} options or callback 1087 | * @param {Function} fn 1088 | * @api public 1089 | */ 1090 | 1091 | exports.renderFile = function(path, options, fn){ 1092 | var key = path + ':string'; 1093 | 1094 | if ('function' == typeof options) { 1095 | fn = options, options = {}; 1096 | } 1097 | 1098 | try { 1099 | options.filename = path; 1100 | var str = options.cache 1101 | ? exports.cache[key] || (exports.cache[key] = fs.readFileSync(path, 'utf8')) 1102 | : fs.readFileSync(path, 'utf8'); 1103 | exports.render(str, options, fn); 1104 | } catch (err) { 1105 | fn(err); 1106 | } 1107 | }; 1108 | 1109 | /** 1110 | * Express support. 1111 | */ 1112 | 1113 | exports.__express = exports.renderFile; 1114 | 1115 | }); // module: jade.js 1116 | 1117 | require.register("lexer.js", function(module, exports, require){ 1118 | /*! 1119 | * Jade - Lexer 1120 | * Copyright(c) 2010 TJ Holowaychuk 1121 | * MIT Licensed 1122 | */ 1123 | 1124 | var utils = require('./utils'); 1125 | 1126 | /** 1127 | * Initialize `Lexer` with the given `str`. 1128 | * 1129 | * Options: 1130 | * 1131 | * - `colons` allow colons for attr delimiters 1132 | * 1133 | * @param {String} str 1134 | * @param {Object} options 1135 | * @api private 1136 | */ 1137 | 1138 | var Lexer = module.exports = function Lexer(str, options) { 1139 | options = options || {}; 1140 | this.input = str.replace(/\r\n|\r/g, '\n'); 1141 | this.colons = options.colons; 1142 | this.deferredTokens = []; 1143 | this.lastIndents = 0; 1144 | this.lineno = 1; 1145 | this.stash = []; 1146 | this.indentStack = []; 1147 | this.indentRe = null; 1148 | this.pipeless = false; 1149 | }; 1150 | 1151 | /** 1152 | * Lexer prototype. 1153 | */ 1154 | 1155 | Lexer.prototype = { 1156 | 1157 | /** 1158 | * Construct a token with the given `type` and `val`. 1159 | * 1160 | * @param {String} type 1161 | * @param {String} val 1162 | * @return {Object} 1163 | * @api private 1164 | */ 1165 | 1166 | tok: function(type, val){ 1167 | return { 1168 | type: type 1169 | , line: this.lineno 1170 | , val: val 1171 | } 1172 | }, 1173 | 1174 | /** 1175 | * Consume the given `len` of input. 1176 | * 1177 | * @param {Number} len 1178 | * @api private 1179 | */ 1180 | 1181 | consume: function(len){ 1182 | this.input = this.input.substr(len); 1183 | }, 1184 | 1185 | /** 1186 | * Scan for `type` with the given `regexp`. 1187 | * 1188 | * @param {String} type 1189 | * @param {RegExp} regexp 1190 | * @return {Object} 1191 | * @api private 1192 | */ 1193 | 1194 | scan: function(regexp, type){ 1195 | var captures; 1196 | if (captures = regexp.exec(this.input)) { 1197 | this.consume(captures[0].length); 1198 | return this.tok(type, captures[1]); 1199 | } 1200 | }, 1201 | 1202 | /** 1203 | * Defer the given `tok`. 1204 | * 1205 | * @param {Object} tok 1206 | * @api private 1207 | */ 1208 | 1209 | defer: function(tok){ 1210 | this.deferredTokens.push(tok); 1211 | }, 1212 | 1213 | /** 1214 | * Lookahead `n` tokens. 1215 | * 1216 | * @param {Number} n 1217 | * @return {Object} 1218 | * @api private 1219 | */ 1220 | 1221 | lookahead: function(n){ 1222 | var fetch = n - this.stash.length; 1223 | while (fetch-- > 0) this.stash.push(this.next()); 1224 | return this.stash[--n]; 1225 | }, 1226 | 1227 | /** 1228 | * Return the indexOf `start` / `end` delimiters. 1229 | * 1230 | * @param {String} start 1231 | * @param {String} end 1232 | * @return {Number} 1233 | * @api private 1234 | */ 1235 | 1236 | indexOfDelimiters: function(start, end){ 1237 | var str = this.input 1238 | , nstart = 0 1239 | , nend = 0 1240 | , pos = 0; 1241 | for (var i = 0, len = str.length; i < len; ++i) { 1242 | if (start == str.charAt(i)) { 1243 | ++nstart; 1244 | } else if (end == str.charAt(i)) { 1245 | if (++nend == nstart) { 1246 | pos = i; 1247 | break; 1248 | } 1249 | } 1250 | } 1251 | return pos; 1252 | }, 1253 | 1254 | /** 1255 | * Stashed token. 1256 | */ 1257 | 1258 | stashed: function() { 1259 | return this.stash.length 1260 | && this.stash.shift(); 1261 | }, 1262 | 1263 | /** 1264 | * Deferred token. 1265 | */ 1266 | 1267 | deferred: function() { 1268 | return this.deferredTokens.length 1269 | && this.deferredTokens.shift(); 1270 | }, 1271 | 1272 | /** 1273 | * end-of-source. 1274 | */ 1275 | 1276 | eos: function() { 1277 | if (this.input.length) return; 1278 | if (this.indentStack.length) { 1279 | this.indentStack.shift(); 1280 | return this.tok('outdent'); 1281 | } else { 1282 | return this.tok('eos'); 1283 | } 1284 | }, 1285 | 1286 | /** 1287 | * Blank line. 1288 | */ 1289 | 1290 | blank: function() { 1291 | var captures; 1292 | if (captures = /^\n *\n/.exec(this.input)) { 1293 | this.consume(captures[0].length - 1); 1294 | 1295 | ++this.lineno; 1296 | if (this.pipeless) return this.tok('text', ''); 1297 | return this.next(); 1298 | } 1299 | }, 1300 | 1301 | /** 1302 | * Comment. 1303 | */ 1304 | 1305 | comment: function() { 1306 | var captures; 1307 | if (captures = /^ *\/\/(-)?([^\n]*)/.exec(this.input)) { 1308 | this.consume(captures[0].length); 1309 | var tok = this.tok('comment', captures[2]); 1310 | tok.buffer = '-' != captures[1]; 1311 | return tok; 1312 | } 1313 | }, 1314 | 1315 | /** 1316 | * Interpolated tag. 1317 | */ 1318 | 1319 | interpolation: function() { 1320 | var captures; 1321 | if (captures = /^#\{(.*?)\}/.exec(this.input)) { 1322 | this.consume(captures[0].length); 1323 | return this.tok('interpolation', captures[1]); 1324 | } 1325 | }, 1326 | 1327 | /** 1328 | * Tag. 1329 | */ 1330 | 1331 | tag: function() { 1332 | var captures; 1333 | if (captures = /^(\w[-:\w]*)(\/?)/.exec(this.input)) { 1334 | this.consume(captures[0].length); 1335 | var tok, name = captures[1]; 1336 | if (':' == name[name.length - 1]) { 1337 | name = name.slice(0, -1); 1338 | tok = this.tok('tag', name); 1339 | this.defer(this.tok(':')); 1340 | while (' ' == this.input[0]) this.input = this.input.substr(1); 1341 | } else { 1342 | tok = this.tok('tag', name); 1343 | } 1344 | tok.selfClosing = !! captures[2]; 1345 | return tok; 1346 | } 1347 | }, 1348 | 1349 | /** 1350 | * Filter. 1351 | */ 1352 | 1353 | filter: function() { 1354 | return this.scan(/^:(\w+)/, 'filter'); 1355 | }, 1356 | 1357 | /** 1358 | * Doctype. 1359 | */ 1360 | 1361 | doctype: function() { 1362 | return this.scan(/^(?:!!!|doctype) *([^\n]+)?/, 'doctype'); 1363 | }, 1364 | 1365 | /** 1366 | * Id. 1367 | */ 1368 | 1369 | id: function() { 1370 | return this.scan(/^#([\w-]+)/, 'id'); 1371 | }, 1372 | 1373 | /** 1374 | * Class. 1375 | */ 1376 | 1377 | className: function() { 1378 | return this.scan(/^\.([\w-]+)/, 'class'); 1379 | }, 1380 | 1381 | /** 1382 | * Text. 1383 | */ 1384 | 1385 | text: function() { 1386 | return this.scan(/^(?:\| ?| ?)?([^\n]+)/, 'text'); 1387 | }, 1388 | 1389 | /** 1390 | * Extends. 1391 | */ 1392 | 1393 | "extends": function() { 1394 | return this.scan(/^extends? +([^\n]+)/, 'extends'); 1395 | }, 1396 | 1397 | /** 1398 | * Block prepend. 1399 | */ 1400 | 1401 | prepend: function() { 1402 | var captures; 1403 | if (captures = /^prepend +([^\n]+)/.exec(this.input)) { 1404 | this.consume(captures[0].length); 1405 | var mode = 'prepend' 1406 | , name = captures[1] 1407 | , tok = this.tok('block', name); 1408 | tok.mode = mode; 1409 | return tok; 1410 | } 1411 | }, 1412 | 1413 | /** 1414 | * Block append. 1415 | */ 1416 | 1417 | append: function() { 1418 | var captures; 1419 | if (captures = /^append +([^\n]+)/.exec(this.input)) { 1420 | this.consume(captures[0].length); 1421 | var mode = 'append' 1422 | , name = captures[1] 1423 | , tok = this.tok('block', name); 1424 | tok.mode = mode; 1425 | return tok; 1426 | } 1427 | }, 1428 | 1429 | /** 1430 | * Block. 1431 | */ 1432 | 1433 | block: function() { 1434 | var captures; 1435 | if (captures = /^block\b *(?:(prepend|append) +)?([^\n]*)/.exec(this.input)) { 1436 | this.consume(captures[0].length); 1437 | var mode = captures[1] || 'replace' 1438 | , name = captures[2] 1439 | , tok = this.tok('block', name); 1440 | 1441 | tok.mode = mode; 1442 | return tok; 1443 | } 1444 | }, 1445 | 1446 | /** 1447 | * Yield. 1448 | */ 1449 | 1450 | yield: function() { 1451 | return this.scan(/^yield */, 'yield'); 1452 | }, 1453 | 1454 | /** 1455 | * Include. 1456 | */ 1457 | 1458 | include: function() { 1459 | return this.scan(/^include +([^\n]+)/, 'include'); 1460 | }, 1461 | 1462 | /** 1463 | * Case. 1464 | */ 1465 | 1466 | "case": function() { 1467 | return this.scan(/^case +([^\n]+)/, 'case'); 1468 | }, 1469 | 1470 | /** 1471 | * When. 1472 | */ 1473 | 1474 | when: function() { 1475 | return this.scan(/^when +([^:\n]+)/, 'when'); 1476 | }, 1477 | 1478 | /** 1479 | * Default. 1480 | */ 1481 | 1482 | "default": function() { 1483 | return this.scan(/^default */, 'default'); 1484 | }, 1485 | 1486 | /** 1487 | * Assignment. 1488 | */ 1489 | 1490 | assignment: function() { 1491 | var captures; 1492 | if (captures = /^(\w+) += *([^;\n]+)( *;? *)/.exec(this.input)) { 1493 | this.consume(captures[0].length); 1494 | var name = captures[1] 1495 | , val = captures[2]; 1496 | return this.tok('code', 'var ' + name + ' = (' + val + ');'); 1497 | } 1498 | }, 1499 | 1500 | /** 1501 | * Call mixin. 1502 | */ 1503 | 1504 | call: function(){ 1505 | var captures; 1506 | if (captures = /^\+([-\w]+)/.exec(this.input)) { 1507 | this.consume(captures[0].length); 1508 | var tok = this.tok('call', captures[1]); 1509 | 1510 | // Check for args (not attributes) 1511 | if (captures = /^ *\((.*?)\)/.exec(this.input)) { 1512 | if (!/^ *[-\w]+ *=/.test(captures[1])) { 1513 | this.consume(captures[0].length); 1514 | tok.args = captures[1]; 1515 | } 1516 | } 1517 | 1518 | return tok; 1519 | } 1520 | }, 1521 | 1522 | /** 1523 | * Mixin. 1524 | */ 1525 | 1526 | mixin: function(){ 1527 | var captures; 1528 | if (captures = /^mixin +([-\w]+)(?: *\((.*)\))?/.exec(this.input)) { 1529 | this.consume(captures[0].length); 1530 | var tok = this.tok('mixin', captures[1]); 1531 | tok.args = captures[2]; 1532 | return tok; 1533 | } 1534 | }, 1535 | 1536 | /** 1537 | * Conditional. 1538 | */ 1539 | 1540 | conditional: function() { 1541 | var captures; 1542 | if (captures = /^(if|unless|else if|else)\b([^\n]*)/.exec(this.input)) { 1543 | this.consume(captures[0].length); 1544 | var type = captures[1] 1545 | , js = captures[2]; 1546 | 1547 | switch (type) { 1548 | case 'if': js = 'if (' + js + ')'; break; 1549 | case 'unless': js = 'if (!(' + js + '))'; break; 1550 | case 'else if': js = 'else if (' + js + ')'; break; 1551 | case 'else': js = 'else'; break; 1552 | } 1553 | 1554 | return this.tok('code', js); 1555 | } 1556 | }, 1557 | 1558 | /** 1559 | * While. 1560 | */ 1561 | 1562 | "while": function() { 1563 | var captures; 1564 | if (captures = /^while +([^\n]+)/.exec(this.input)) { 1565 | this.consume(captures[0].length); 1566 | return this.tok('code', 'while (' + captures[1] + ')'); 1567 | } 1568 | }, 1569 | 1570 | /** 1571 | * Each. 1572 | */ 1573 | 1574 | each: function() { 1575 | var captures; 1576 | if (captures = /^(?:- *)?(?:each|for) +(\w+)(?: *, *(\w+))? * in *([^\n]+)/.exec(this.input)) { 1577 | this.consume(captures[0].length); 1578 | var tok = this.tok('each', captures[1]); 1579 | tok.key = captures[2] || '$index'; 1580 | tok.code = captures[3]; 1581 | return tok; 1582 | } 1583 | }, 1584 | 1585 | /** 1586 | * Code. 1587 | */ 1588 | 1589 | code: function() { 1590 | var captures; 1591 | if (captures = /^(!?=|-)([^\n]+)/.exec(this.input)) { 1592 | this.consume(captures[0].length); 1593 | var flags = captures[1]; 1594 | captures[1] = captures[2]; 1595 | var tok = this.tok('code', captures[1]); 1596 | tok.escape = flags.charAt(0) === '='; 1597 | tok.buffer = flags.charAt(0) === '=' || flags.charAt(1) === '='; 1598 | return tok; 1599 | } 1600 | }, 1601 | 1602 | /** 1603 | * Attributes. 1604 | */ 1605 | 1606 | attrs: function() { 1607 | if ('(' == this.input.charAt(0)) { 1608 | var index = this.indexOfDelimiters('(', ')') 1609 | , str = this.input.substr(1, index-1) 1610 | , tok = this.tok('attrs') 1611 | , len = str.length 1612 | , colons = this.colons 1613 | , states = ['key'] 1614 | , escapedAttr 1615 | , key = '' 1616 | , val = '' 1617 | , quote 1618 | , c 1619 | , p; 1620 | 1621 | function state(){ 1622 | return states[states.length - 1]; 1623 | } 1624 | 1625 | function interpolate(attr) { 1626 | return attr.replace(/(\\)?#\{([^}]+)\}/g, function(_, escape, expr){ 1627 | return escape 1628 | ? _ 1629 | : quote + " + (" + expr + ") + " + quote; 1630 | }); 1631 | } 1632 | 1633 | this.consume(index + 1); 1634 | tok.attrs = {}; 1635 | tok.escaped = {}; 1636 | 1637 | function parse(c) { 1638 | var real = c; 1639 | // TODO: remove when people fix ":" 1640 | if (colons && ':' == c) c = '='; 1641 | switch (c) { 1642 | case ',': 1643 | case '\n': 1644 | switch (state()) { 1645 | case 'expr': 1646 | case 'array': 1647 | case 'string': 1648 | case 'object': 1649 | val += c; 1650 | break; 1651 | default: 1652 | states.push('key'); 1653 | val = val.trim(); 1654 | key = key.trim(); 1655 | if ('' == key) return; 1656 | key = key.replace(/^['"]|['"]$/g, '').replace('!', ''); 1657 | tok.escaped[key] = escapedAttr; 1658 | tok.attrs[key] = '' == val 1659 | ? true 1660 | : interpolate(val); 1661 | key = val = ''; 1662 | } 1663 | break; 1664 | case '=': 1665 | switch (state()) { 1666 | case 'key char': 1667 | key += real; 1668 | break; 1669 | case 'val': 1670 | case 'expr': 1671 | case 'array': 1672 | case 'string': 1673 | case 'object': 1674 | val += real; 1675 | break; 1676 | default: 1677 | escapedAttr = '!' != p; 1678 | states.push('val'); 1679 | } 1680 | break; 1681 | case '(': 1682 | if ('val' == state() 1683 | || 'expr' == state()) states.push('expr'); 1684 | val += c; 1685 | break; 1686 | case ')': 1687 | if ('expr' == state() 1688 | || 'val' == state()) states.pop(); 1689 | val += c; 1690 | break; 1691 | case '{': 1692 | if ('val' == state()) states.push('object'); 1693 | val += c; 1694 | break; 1695 | case '}': 1696 | if ('object' == state()) states.pop(); 1697 | val += c; 1698 | break; 1699 | case '[': 1700 | if ('val' == state()) states.push('array'); 1701 | val += c; 1702 | break; 1703 | case ']': 1704 | if ('array' == state()) states.pop(); 1705 | val += c; 1706 | break; 1707 | case '"': 1708 | case "'": 1709 | switch (state()) { 1710 | case 'key': 1711 | states.push('key char'); 1712 | break; 1713 | case 'key char': 1714 | states.pop(); 1715 | break; 1716 | case 'string': 1717 | if (c == quote) states.pop(); 1718 | val += c; 1719 | break; 1720 | default: 1721 | states.push('string'); 1722 | val += c; 1723 | quote = c; 1724 | } 1725 | break; 1726 | case '': 1727 | break; 1728 | default: 1729 | switch (state()) { 1730 | case 'key': 1731 | case 'key char': 1732 | key += c; 1733 | break; 1734 | default: 1735 | val += c; 1736 | } 1737 | } 1738 | p = c; 1739 | } 1740 | 1741 | for (var i = 0; i < len; ++i) { 1742 | parse(str.charAt(i)); 1743 | } 1744 | 1745 | parse(','); 1746 | 1747 | if ('/' == this.input.charAt(0)) { 1748 | this.consume(1); 1749 | tok.selfClosing = true; 1750 | } 1751 | 1752 | return tok; 1753 | } 1754 | }, 1755 | 1756 | /** 1757 | * Indent | Outdent | Newline. 1758 | */ 1759 | 1760 | indent: function() { 1761 | var captures, re; 1762 | 1763 | // established regexp 1764 | if (this.indentRe) { 1765 | captures = this.indentRe.exec(this.input); 1766 | // determine regexp 1767 | } else { 1768 | // tabs 1769 | re = /^\n(\t*) */; 1770 | captures = re.exec(this.input); 1771 | 1772 | // spaces 1773 | if (captures && !captures[1].length) { 1774 | re = /^\n( *)/; 1775 | captures = re.exec(this.input); 1776 | } 1777 | 1778 | // established 1779 | if (captures && captures[1].length) this.indentRe = re; 1780 | } 1781 | 1782 | if (captures) { 1783 | var tok 1784 | , indents = captures[1].length; 1785 | 1786 | ++this.lineno; 1787 | this.consume(indents + 1); 1788 | 1789 | if (' ' == this.input[0] || '\t' == this.input[0]) { 1790 | throw new Error('Invalid indentation, you can use tabs or spaces but not both'); 1791 | } 1792 | 1793 | // blank line 1794 | if ('\n' == this.input[0]) return this.tok('newline'); 1795 | 1796 | // outdent 1797 | if (this.indentStack.length && indents < this.indentStack[0]) { 1798 | while (this.indentStack.length && this.indentStack[0] > indents) { 1799 | this.stash.push(this.tok('outdent')); 1800 | this.indentStack.shift(); 1801 | } 1802 | tok = this.stash.pop(); 1803 | // indent 1804 | } else if (indents && indents != this.indentStack[0]) { 1805 | this.indentStack.unshift(indents); 1806 | tok = this.tok('indent', indents); 1807 | // newline 1808 | } else { 1809 | tok = this.tok('newline'); 1810 | } 1811 | 1812 | return tok; 1813 | } 1814 | }, 1815 | 1816 | /** 1817 | * Pipe-less text consumed only when 1818 | * pipeless is true; 1819 | */ 1820 | 1821 | pipelessText: function() { 1822 | if (this.pipeless) { 1823 | if ('\n' == this.input[0]) return; 1824 | var i = this.input.indexOf('\n'); 1825 | if (-1 == i) i = this.input.length; 1826 | var str = this.input.substr(0, i); 1827 | this.consume(str.length); 1828 | return this.tok('text', str); 1829 | } 1830 | }, 1831 | 1832 | /** 1833 | * ':' 1834 | */ 1835 | 1836 | colon: function() { 1837 | return this.scan(/^: */, ':'); 1838 | }, 1839 | 1840 | /** 1841 | * Return the next token object, or those 1842 | * previously stashed by lookahead. 1843 | * 1844 | * @return {Object} 1845 | * @api private 1846 | */ 1847 | 1848 | advance: function(){ 1849 | return this.stashed() 1850 | || this.next(); 1851 | }, 1852 | 1853 | /** 1854 | * Return the next token object. 1855 | * 1856 | * @return {Object} 1857 | * @api private 1858 | */ 1859 | 1860 | next: function() { 1861 | return this.deferred() 1862 | || this.blank() 1863 | || this.eos() 1864 | || this.pipelessText() 1865 | || this.yield() 1866 | || this.doctype() 1867 | || this.interpolation() 1868 | || this["case"]() 1869 | || this.when() 1870 | || this["default"]() 1871 | || this["extends"]() 1872 | || this.append() 1873 | || this.prepend() 1874 | || this.block() 1875 | || this.include() 1876 | || this.mixin() 1877 | || this.call() 1878 | || this.conditional() 1879 | || this.each() 1880 | || this["while"]() 1881 | || this.assignment() 1882 | || this.tag() 1883 | || this.filter() 1884 | || this.code() 1885 | || this.id() 1886 | || this.className() 1887 | || this.attrs() 1888 | || this.indent() 1889 | || this.comment() 1890 | || this.colon() 1891 | || this.text(); 1892 | } 1893 | }; 1894 | 1895 | }); // module: lexer.js 1896 | 1897 | require.register("nodes/attrs.js", function(module, exports, require){ 1898 | 1899 | /*! 1900 | * Jade - nodes - Attrs 1901 | * Copyright(c) 2010 TJ Holowaychuk 1902 | * MIT Licensed 1903 | */ 1904 | 1905 | /** 1906 | * Module dependencies. 1907 | */ 1908 | 1909 | var Node = require('./node'), 1910 | Block = require('./block'); 1911 | 1912 | /** 1913 | * Initialize a `Attrs` node. 1914 | * 1915 | * @api public 1916 | */ 1917 | 1918 | var Attrs = module.exports = function Attrs() { 1919 | this.attrs = []; 1920 | }; 1921 | 1922 | /** 1923 | * Inherit from `Node`. 1924 | */ 1925 | 1926 | Attrs.prototype = new Node; 1927 | Attrs.prototype.constructor = Attrs; 1928 | 1929 | 1930 | /** 1931 | * Set attribute `name` to `val`, keep in mind these become 1932 | * part of a raw js object literal, so to quote a value you must 1933 | * '"quote me"', otherwise or example 'user.name' is literal JavaScript. 1934 | * 1935 | * @param {String} name 1936 | * @param {String} val 1937 | * @param {Boolean} escaped 1938 | * @return {Tag} for chaining 1939 | * @api public 1940 | */ 1941 | 1942 | Attrs.prototype.setAttribute = function(name, val, escaped){ 1943 | this.attrs.push({ name: name, val: val, escaped: escaped }); 1944 | return this; 1945 | }; 1946 | 1947 | /** 1948 | * Remove attribute `name` when present. 1949 | * 1950 | * @param {String} name 1951 | * @api public 1952 | */ 1953 | 1954 | Attrs.prototype.removeAttribute = function(name){ 1955 | for (var i = 0, len = this.attrs.length; i < len; ++i) { 1956 | if (this.attrs[i] && this.attrs[i].name == name) { 1957 | delete this.attrs[i]; 1958 | } 1959 | } 1960 | }; 1961 | 1962 | /** 1963 | * Get attribute value by `name`. 1964 | * 1965 | * @param {String} name 1966 | * @return {String} 1967 | * @api public 1968 | */ 1969 | 1970 | Attrs.prototype.getAttribute = function(name){ 1971 | for (var i = 0, len = this.attrs.length; i < len; ++i) { 1972 | if (this.attrs[i] && this.attrs[i].name == name) { 1973 | return this.attrs[i].val; 1974 | } 1975 | } 1976 | }; 1977 | 1978 | }); // module: nodes/attrs.js 1979 | 1980 | require.register("nodes/block-comment.js", function(module, exports, require){ 1981 | 1982 | /*! 1983 | * Jade - nodes - BlockComment 1984 | * Copyright(c) 2010 TJ Holowaychuk 1985 | * MIT Licensed 1986 | */ 1987 | 1988 | /** 1989 | * Module dependencies. 1990 | */ 1991 | 1992 | var Node = require('./node'); 1993 | 1994 | /** 1995 | * Initialize a `BlockComment` with the given `block`. 1996 | * 1997 | * @param {String} val 1998 | * @param {Block} block 1999 | * @param {Boolean} buffer 2000 | * @api public 2001 | */ 2002 | 2003 | var BlockComment = module.exports = function BlockComment(val, block, buffer) { 2004 | this.block = block; 2005 | this.val = val; 2006 | this.buffer = buffer; 2007 | }; 2008 | 2009 | /** 2010 | * Inherit from `Node`. 2011 | */ 2012 | 2013 | BlockComment.prototype = new Node; 2014 | BlockComment.prototype.constructor = BlockComment; 2015 | 2016 | }); // module: nodes/block-comment.js 2017 | 2018 | require.register("nodes/block.js", function(module, exports, require){ 2019 | 2020 | /*! 2021 | * Jade - nodes - Block 2022 | * Copyright(c) 2010 TJ Holowaychuk 2023 | * MIT Licensed 2024 | */ 2025 | 2026 | /** 2027 | * Module dependencies. 2028 | */ 2029 | 2030 | var Node = require('./node'); 2031 | 2032 | /** 2033 | * Initialize a new `Block` with an optional `node`. 2034 | * 2035 | * @param {Node} node 2036 | * @api public 2037 | */ 2038 | 2039 | var Block = module.exports = function Block(node){ 2040 | this.nodes = []; 2041 | if (node) this.push(node); 2042 | }; 2043 | 2044 | /** 2045 | * Inherit from `Node`. 2046 | */ 2047 | 2048 | Block.prototype = new Node; 2049 | Block.prototype.constructor = Block; 2050 | 2051 | 2052 | /** 2053 | * Block flag. 2054 | */ 2055 | 2056 | Block.prototype.isBlock = true; 2057 | 2058 | /** 2059 | * Replace the nodes in `other` with the nodes 2060 | * in `this` block. 2061 | * 2062 | * @param {Block} other 2063 | * @api private 2064 | */ 2065 | 2066 | Block.prototype.replace = function(other){ 2067 | other.nodes = this.nodes; 2068 | }; 2069 | 2070 | /** 2071 | * Pust the given `node`. 2072 | * 2073 | * @param {Node} node 2074 | * @return {Number} 2075 | * @api public 2076 | */ 2077 | 2078 | Block.prototype.push = function(node){ 2079 | return this.nodes.push(node); 2080 | }; 2081 | 2082 | /** 2083 | * Check if this block is empty. 2084 | * 2085 | * @return {Boolean} 2086 | * @api public 2087 | */ 2088 | 2089 | Block.prototype.isEmpty = function(){ 2090 | return 0 == this.nodes.length; 2091 | }; 2092 | 2093 | /** 2094 | * Unshift the given `node`. 2095 | * 2096 | * @param {Node} node 2097 | * @return {Number} 2098 | * @api public 2099 | */ 2100 | 2101 | Block.prototype.unshift = function(node){ 2102 | return this.nodes.unshift(node); 2103 | }; 2104 | 2105 | /** 2106 | * Return the "last" block, or the first `yield` node. 2107 | * 2108 | * @return {Block} 2109 | * @api private 2110 | */ 2111 | 2112 | Block.prototype.includeBlock = function(){ 2113 | var ret = this 2114 | , node; 2115 | 2116 | for (var i = 0, len = this.nodes.length; i < len; ++i) { 2117 | node = this.nodes[i]; 2118 | if (node.yield) return node; 2119 | else if (node.textOnly) continue; 2120 | else if (node.includeBlock) ret = node.includeBlock(); 2121 | else if (node.block && !node.block.isEmpty()) ret = node.block.includeBlock(); 2122 | if (ret.yield) return ret; 2123 | } 2124 | 2125 | return ret; 2126 | }; 2127 | 2128 | /** 2129 | * Return a clone of this block. 2130 | * 2131 | * @return {Block} 2132 | * @api private 2133 | */ 2134 | 2135 | Block.prototype.clone = function(){ 2136 | var clone = new Block; 2137 | for (var i = 0, len = this.nodes.length; i < len; ++i) { 2138 | clone.push(this.nodes[i].clone()); 2139 | } 2140 | return clone; 2141 | }; 2142 | 2143 | 2144 | }); // module: nodes/block.js 2145 | 2146 | require.register("nodes/case.js", function(module, exports, require){ 2147 | 2148 | /*! 2149 | * Jade - nodes - Case 2150 | * Copyright(c) 2010 TJ Holowaychuk 2151 | * MIT Licensed 2152 | */ 2153 | 2154 | /** 2155 | * Module dependencies. 2156 | */ 2157 | 2158 | var Node = require('./node'); 2159 | 2160 | /** 2161 | * Initialize a new `Case` with `expr`. 2162 | * 2163 | * @param {String} expr 2164 | * @api public 2165 | */ 2166 | 2167 | var Case = exports = module.exports = function Case(expr, block){ 2168 | this.expr = expr; 2169 | this.block = block; 2170 | }; 2171 | 2172 | /** 2173 | * Inherit from `Node`. 2174 | */ 2175 | 2176 | Case.prototype = new Node; 2177 | Case.prototype.constructor = Case; 2178 | 2179 | 2180 | var When = exports.When = function When(expr, block){ 2181 | this.expr = expr; 2182 | this.block = block; 2183 | this.debug = false; 2184 | }; 2185 | 2186 | /** 2187 | * Inherit from `Node`. 2188 | */ 2189 | 2190 | When.prototype = new Node; 2191 | When.prototype.constructor = When; 2192 | 2193 | 2194 | 2195 | }); // module: nodes/case.js 2196 | 2197 | require.register("nodes/code.js", function(module, exports, require){ 2198 | 2199 | /*! 2200 | * Jade - nodes - Code 2201 | * Copyright(c) 2010 TJ Holowaychuk 2202 | * MIT Licensed 2203 | */ 2204 | 2205 | /** 2206 | * Module dependencies. 2207 | */ 2208 | 2209 | var Node = require('./node'); 2210 | 2211 | /** 2212 | * Initialize a `Code` node with the given code `val`. 2213 | * Code may also be optionally buffered and escaped. 2214 | * 2215 | * @param {String} val 2216 | * @param {Boolean} buffer 2217 | * @param {Boolean} escape 2218 | * @api public 2219 | */ 2220 | 2221 | var Code = module.exports = function Code(val, buffer, escape) { 2222 | this.val = val; 2223 | this.buffer = buffer; 2224 | this.escape = escape; 2225 | if (val.match(/^ *else/)) this.debug = false; 2226 | }; 2227 | 2228 | /** 2229 | * Inherit from `Node`. 2230 | */ 2231 | 2232 | Code.prototype = new Node; 2233 | Code.prototype.constructor = Code; 2234 | 2235 | }); // module: nodes/code.js 2236 | 2237 | require.register("nodes/comment.js", function(module, exports, require){ 2238 | 2239 | /*! 2240 | * Jade - nodes - Comment 2241 | * Copyright(c) 2010 TJ Holowaychuk 2242 | * MIT Licensed 2243 | */ 2244 | 2245 | /** 2246 | * Module dependencies. 2247 | */ 2248 | 2249 | var Node = require('./node'); 2250 | 2251 | /** 2252 | * Initialize a `Comment` with the given `val`, optionally `buffer`, 2253 | * otherwise the comment may render in the output. 2254 | * 2255 | * @param {String} val 2256 | * @param {Boolean} buffer 2257 | * @api public 2258 | */ 2259 | 2260 | var Comment = module.exports = function Comment(val, buffer) { 2261 | this.val = val; 2262 | this.buffer = buffer; 2263 | }; 2264 | 2265 | /** 2266 | * Inherit from `Node`. 2267 | */ 2268 | 2269 | Comment.prototype = new Node; 2270 | Comment.prototype.constructor = Comment; 2271 | 2272 | }); // module: nodes/comment.js 2273 | 2274 | require.register("nodes/doctype.js", function(module, exports, require){ 2275 | 2276 | /*! 2277 | * Jade - nodes - Doctype 2278 | * Copyright(c) 2010 TJ Holowaychuk 2279 | * MIT Licensed 2280 | */ 2281 | 2282 | /** 2283 | * Module dependencies. 2284 | */ 2285 | 2286 | var Node = require('./node'); 2287 | 2288 | /** 2289 | * Initialize a `Doctype` with the given `val`. 2290 | * 2291 | * @param {String} val 2292 | * @api public 2293 | */ 2294 | 2295 | var Doctype = module.exports = function Doctype(val) { 2296 | this.val = val; 2297 | }; 2298 | 2299 | /** 2300 | * Inherit from `Node`. 2301 | */ 2302 | 2303 | Doctype.prototype = new Node; 2304 | Doctype.prototype.constructor = Doctype; 2305 | 2306 | }); // module: nodes/doctype.js 2307 | 2308 | require.register("nodes/each.js", function(module, exports, require){ 2309 | 2310 | /*! 2311 | * Jade - nodes - Each 2312 | * Copyright(c) 2010 TJ Holowaychuk 2313 | * MIT Licensed 2314 | */ 2315 | 2316 | /** 2317 | * Module dependencies. 2318 | */ 2319 | 2320 | var Node = require('./node'); 2321 | 2322 | /** 2323 | * Initialize an `Each` node, representing iteration 2324 | * 2325 | * @param {String} obj 2326 | * @param {String} val 2327 | * @param {String} key 2328 | * @param {Block} block 2329 | * @api public 2330 | */ 2331 | 2332 | var Each = module.exports = function Each(obj, val, key, block) { 2333 | this.obj = obj; 2334 | this.val = val; 2335 | this.key = key; 2336 | this.block = block; 2337 | }; 2338 | 2339 | /** 2340 | * Inherit from `Node`. 2341 | */ 2342 | 2343 | Each.prototype = new Node; 2344 | Each.prototype.constructor = Each; 2345 | 2346 | }); // module: nodes/each.js 2347 | 2348 | require.register("nodes/filter.js", function(module, exports, require){ 2349 | 2350 | /*! 2351 | * Jade - nodes - Filter 2352 | * Copyright(c) 2010 TJ Holowaychuk 2353 | * MIT Licensed 2354 | */ 2355 | 2356 | /** 2357 | * Module dependencies. 2358 | */ 2359 | 2360 | var Node = require('./node') 2361 | , Block = require('./block'); 2362 | 2363 | /** 2364 | * Initialize a `Filter` node with the given 2365 | * filter `name` and `block`. 2366 | * 2367 | * @param {String} name 2368 | * @param {Block|Node} block 2369 | * @api public 2370 | */ 2371 | 2372 | var Filter = module.exports = function Filter(name, block, attrs) { 2373 | this.name = name; 2374 | this.block = block; 2375 | this.attrs = attrs; 2376 | this.isASTFilter = !block.nodes.every(function(node){ return node.isText }); 2377 | }; 2378 | 2379 | /** 2380 | * Inherit from `Node`. 2381 | */ 2382 | 2383 | Filter.prototype = new Node; 2384 | Filter.prototype.constructor = Filter; 2385 | 2386 | }); // module: nodes/filter.js 2387 | 2388 | require.register("nodes/index.js", function(module, exports, require){ 2389 | 2390 | /*! 2391 | * Jade - nodes 2392 | * Copyright(c) 2010 TJ Holowaychuk 2393 | * MIT Licensed 2394 | */ 2395 | 2396 | exports.Node = require('./node'); 2397 | exports.Tag = require('./tag'); 2398 | exports.Code = require('./code'); 2399 | exports.Each = require('./each'); 2400 | exports.Case = require('./case'); 2401 | exports.Text = require('./text'); 2402 | exports.Block = require('./block'); 2403 | exports.Mixin = require('./mixin'); 2404 | exports.Filter = require('./filter'); 2405 | exports.Comment = require('./comment'); 2406 | exports.Literal = require('./literal'); 2407 | exports.BlockComment = require('./block-comment'); 2408 | exports.Doctype = require('./doctype'); 2409 | 2410 | }); // module: nodes/index.js 2411 | 2412 | require.register("nodes/literal.js", function(module, exports, require){ 2413 | 2414 | /*! 2415 | * Jade - nodes - Literal 2416 | * Copyright(c) 2010 TJ Holowaychuk 2417 | * MIT Licensed 2418 | */ 2419 | 2420 | /** 2421 | * Module dependencies. 2422 | */ 2423 | 2424 | var Node = require('./node'); 2425 | 2426 | /** 2427 | * Initialize a `Literal` node with the given `str. 2428 | * 2429 | * @param {String} str 2430 | * @api public 2431 | */ 2432 | 2433 | var Literal = module.exports = function Literal(str) { 2434 | this.str = str 2435 | .replace(/\\/g, "\\\\") 2436 | .replace(/\n|\r\n/g, "\\n") 2437 | .replace(/'/g, "\\'"); 2438 | }; 2439 | 2440 | /** 2441 | * Inherit from `Node`. 2442 | */ 2443 | 2444 | Literal.prototype = new Node; 2445 | Literal.prototype.constructor = Literal; 2446 | 2447 | 2448 | }); // module: nodes/literal.js 2449 | 2450 | require.register("nodes/mixin.js", function(module, exports, require){ 2451 | 2452 | /*! 2453 | * Jade - nodes - Mixin 2454 | * Copyright(c) 2010 TJ Holowaychuk 2455 | * MIT Licensed 2456 | */ 2457 | 2458 | /** 2459 | * Module dependencies. 2460 | */ 2461 | 2462 | var Attrs = require('./attrs'); 2463 | 2464 | /** 2465 | * Initialize a new `Mixin` with `name` and `block`. 2466 | * 2467 | * @param {String} name 2468 | * @param {String} args 2469 | * @param {Block} block 2470 | * @api public 2471 | */ 2472 | 2473 | var Mixin = module.exports = function Mixin(name, args, block, call){ 2474 | this.name = name; 2475 | this.args = args; 2476 | this.block = block; 2477 | this.attrs = []; 2478 | this.call = call; 2479 | }; 2480 | 2481 | /** 2482 | * Inherit from `Attrs`. 2483 | */ 2484 | 2485 | Mixin.prototype = new Attrs; 2486 | Mixin.prototype.constructor = Mixin; 2487 | 2488 | 2489 | 2490 | }); // module: nodes/mixin.js 2491 | 2492 | require.register("nodes/node.js", function(module, exports, require){ 2493 | 2494 | /*! 2495 | * Jade - nodes - Node 2496 | * Copyright(c) 2010 TJ Holowaychuk 2497 | * MIT Licensed 2498 | */ 2499 | 2500 | /** 2501 | * Initialize a `Node`. 2502 | * 2503 | * @api public 2504 | */ 2505 | 2506 | var Node = module.exports = function Node(){}; 2507 | 2508 | /** 2509 | * Clone this node (return itself) 2510 | * 2511 | * @return {Node} 2512 | * @api private 2513 | */ 2514 | 2515 | Node.prototype.clone = function(){ 2516 | return this; 2517 | }; 2518 | 2519 | }); // module: nodes/node.js 2520 | 2521 | require.register("nodes/tag.js", function(module, exports, require){ 2522 | 2523 | /*! 2524 | * Jade - nodes - Tag 2525 | * Copyright(c) 2010 TJ Holowaychuk 2526 | * MIT Licensed 2527 | */ 2528 | 2529 | /** 2530 | * Module dependencies. 2531 | */ 2532 | 2533 | var Attrs = require('./attrs'), 2534 | Block = require('./block'), 2535 | inlineTags = require('../inline-tags'); 2536 | 2537 | /** 2538 | * Initialize a `Tag` node with the given tag `name` and optional `block`. 2539 | * 2540 | * @param {String} name 2541 | * @param {Block} block 2542 | * @api public 2543 | */ 2544 | 2545 | var Tag = module.exports = function Tag(name, block) { 2546 | this.name = name; 2547 | this.attrs = []; 2548 | this.block = block || new Block; 2549 | }; 2550 | 2551 | /** 2552 | * Inherit from `Attrs`. 2553 | */ 2554 | 2555 | Tag.prototype = new Attrs; 2556 | Tag.prototype.constructor = Tag; 2557 | 2558 | 2559 | /** 2560 | * Clone this tag. 2561 | * 2562 | * @return {Tag} 2563 | * @api private 2564 | */ 2565 | 2566 | Tag.prototype.clone = function(){ 2567 | var clone = new Tag(this.name, this.block.clone()); 2568 | clone.line = this.line; 2569 | clone.attrs = this.attrs; 2570 | clone.textOnly = this.textOnly; 2571 | return clone; 2572 | }; 2573 | 2574 | /** 2575 | * Check if this tag is an inline tag. 2576 | * 2577 | * @return {Boolean} 2578 | * @api private 2579 | */ 2580 | 2581 | Tag.prototype.isInline = function(){ 2582 | return ~inlineTags.indexOf(this.name); 2583 | }; 2584 | 2585 | /** 2586 | * Check if this tag's contents can be inlined. Used for pretty printing. 2587 | * 2588 | * @return {Boolean} 2589 | * @api private 2590 | */ 2591 | 2592 | Tag.prototype.canInline = function(){ 2593 | var nodes = this.block.nodes; 2594 | 2595 | function isInline(node){ 2596 | // Recurse if the node is a block 2597 | if (node.isBlock) return node.nodes.every(isInline); 2598 | return node.isText || (node.isInline && node.isInline()); 2599 | } 2600 | 2601 | // Empty tag 2602 | if (!nodes.length) return true; 2603 | 2604 | // Text-only or inline-only tag 2605 | if (1 == nodes.length) return isInline(nodes[0]); 2606 | 2607 | // Multi-line inline-only tag 2608 | if (this.block.nodes.every(isInline)) { 2609 | for (var i = 1, len = nodes.length; i < len; ++i) { 2610 | if (nodes[i-1].isText && nodes[i].isText) 2611 | return false; 2612 | } 2613 | return true; 2614 | } 2615 | 2616 | // Mixed tag 2617 | return false; 2618 | }; 2619 | }); // module: nodes/tag.js 2620 | 2621 | require.register("nodes/text.js", function(module, exports, require){ 2622 | 2623 | /*! 2624 | * Jade - nodes - Text 2625 | * Copyright(c) 2010 TJ Holowaychuk 2626 | * MIT Licensed 2627 | */ 2628 | 2629 | /** 2630 | * Module dependencies. 2631 | */ 2632 | 2633 | var Node = require('./node'); 2634 | 2635 | /** 2636 | * Initialize a `Text` node with optional `line`. 2637 | * 2638 | * @param {String} line 2639 | * @api public 2640 | */ 2641 | 2642 | var Text = module.exports = function Text(line) { 2643 | this.val = ''; 2644 | if ('string' == typeof line) this.val = line; 2645 | }; 2646 | 2647 | /** 2648 | * Inherit from `Node`. 2649 | */ 2650 | 2651 | Text.prototype = new Node; 2652 | Text.prototype.constructor = Text; 2653 | 2654 | 2655 | /** 2656 | * Flag as text. 2657 | */ 2658 | 2659 | Text.prototype.isText = true; 2660 | }); // module: nodes/text.js 2661 | 2662 | require.register("parser.js", function(module, exports, require){ 2663 | 2664 | /*! 2665 | * Jade - Parser 2666 | * Copyright(c) 2010 TJ Holowaychuk 2667 | * MIT Licensed 2668 | */ 2669 | 2670 | /** 2671 | * Module dependencies. 2672 | */ 2673 | 2674 | var Lexer = require('./lexer') 2675 | , nodes = require('./nodes') 2676 | , utils = require('./utils'); 2677 | 2678 | /** 2679 | * Initialize `Parser` with the given input `str` and `filename`. 2680 | * 2681 | * @param {String} str 2682 | * @param {String} filename 2683 | * @param {Object} options 2684 | * @api public 2685 | */ 2686 | 2687 | var Parser = exports = module.exports = function Parser(str, filename, options){ 2688 | this.input = str; 2689 | this.lexer = new Lexer(str, options); 2690 | this.filename = filename; 2691 | this.blocks = {}; 2692 | this.mixins = {}; 2693 | this.options = options; 2694 | this.contexts = [this]; 2695 | }; 2696 | 2697 | /** 2698 | * Tags that may not contain tags. 2699 | */ 2700 | 2701 | var textOnly = exports.textOnly = ['script', 'style']; 2702 | 2703 | /** 2704 | * Parser prototype. 2705 | */ 2706 | 2707 | Parser.prototype = { 2708 | 2709 | /** 2710 | * Push `parser` onto the context stack, 2711 | * or pop and return a `Parser`. 2712 | */ 2713 | 2714 | context: function(parser){ 2715 | if (parser) { 2716 | this.contexts.push(parser); 2717 | } else { 2718 | return this.contexts.pop(); 2719 | } 2720 | }, 2721 | 2722 | /** 2723 | * Return the next token object. 2724 | * 2725 | * @return {Object} 2726 | * @api private 2727 | */ 2728 | 2729 | advance: function(){ 2730 | return this.lexer.advance(); 2731 | }, 2732 | 2733 | /** 2734 | * Skip `n` tokens. 2735 | * 2736 | * @param {Number} n 2737 | * @api private 2738 | */ 2739 | 2740 | skip: function(n){ 2741 | while (n--) this.advance(); 2742 | }, 2743 | 2744 | /** 2745 | * Single token lookahead. 2746 | * 2747 | * @return {Object} 2748 | * @api private 2749 | */ 2750 | 2751 | peek: function() { 2752 | return this.lookahead(1); 2753 | }, 2754 | 2755 | /** 2756 | * Return lexer lineno. 2757 | * 2758 | * @return {Number} 2759 | * @api private 2760 | */ 2761 | 2762 | line: function() { 2763 | return this.lexer.lineno; 2764 | }, 2765 | 2766 | /** 2767 | * `n` token lookahead. 2768 | * 2769 | * @param {Number} n 2770 | * @return {Object} 2771 | * @api private 2772 | */ 2773 | 2774 | lookahead: function(n){ 2775 | return this.lexer.lookahead(n); 2776 | }, 2777 | 2778 | /** 2779 | * Parse input returning a string of js for evaluation. 2780 | * 2781 | * @return {String} 2782 | * @api public 2783 | */ 2784 | 2785 | parse: function(){ 2786 | var block = new nodes.Block, parser; 2787 | block.line = this.line(); 2788 | 2789 | while ('eos' != this.peek().type) { 2790 | if ('newline' == this.peek().type) { 2791 | this.advance(); 2792 | } else { 2793 | block.push(this.parseExpr()); 2794 | } 2795 | } 2796 | 2797 | if (parser = this.extending) { 2798 | this.context(parser); 2799 | var ast = parser.parse(); 2800 | this.context(); 2801 | // hoist mixins 2802 | for (var name in this.mixins) 2803 | ast.unshift(this.mixins[name]); 2804 | return ast; 2805 | } 2806 | 2807 | return block; 2808 | }, 2809 | 2810 | /** 2811 | * Expect the given type, or throw an exception. 2812 | * 2813 | * @param {String} type 2814 | * @api private 2815 | */ 2816 | 2817 | expect: function(type){ 2818 | if (this.peek().type === type) { 2819 | return this.advance(); 2820 | } else { 2821 | throw new Error('expected "' + type + '", but got "' + this.peek().type + '"'); 2822 | } 2823 | }, 2824 | 2825 | /** 2826 | * Accept the given `type`. 2827 | * 2828 | * @param {String} type 2829 | * @api private 2830 | */ 2831 | 2832 | accept: function(type){ 2833 | if (this.peek().type === type) { 2834 | return this.advance(); 2835 | } 2836 | }, 2837 | 2838 | /** 2839 | * tag 2840 | * | doctype 2841 | * | mixin 2842 | * | include 2843 | * | filter 2844 | * | comment 2845 | * | text 2846 | * | each 2847 | * | code 2848 | * | yield 2849 | * | id 2850 | * | class 2851 | * | interpolation 2852 | */ 2853 | 2854 | parseExpr: function(){ 2855 | switch (this.peek().type) { 2856 | case 'tag': 2857 | return this.parseTag(); 2858 | case 'mixin': 2859 | return this.parseMixin(); 2860 | case 'block': 2861 | return this.parseBlock(); 2862 | case 'case': 2863 | return this.parseCase(); 2864 | case 'when': 2865 | return this.parseWhen(); 2866 | case 'default': 2867 | return this.parseDefault(); 2868 | case 'extends': 2869 | return this.parseExtends(); 2870 | case 'include': 2871 | return this.parseInclude(); 2872 | case 'doctype': 2873 | return this.parseDoctype(); 2874 | case 'filter': 2875 | return this.parseFilter(); 2876 | case 'comment': 2877 | return this.parseComment(); 2878 | case 'text': 2879 | return this.parseText(); 2880 | case 'each': 2881 | return this.parseEach(); 2882 | case 'code': 2883 | return this.parseCode(); 2884 | case 'call': 2885 | return this.parseCall(); 2886 | case 'interpolation': 2887 | return this.parseInterpolation(); 2888 | case 'yield': 2889 | this.advance(); 2890 | var block = new nodes.Block; 2891 | block.yield = true; 2892 | return block; 2893 | case 'id': 2894 | case 'class': 2895 | var tok = this.advance(); 2896 | this.lexer.defer(this.lexer.tok('tag', 'div')); 2897 | this.lexer.defer(tok); 2898 | return this.parseExpr(); 2899 | default: 2900 | throw new Error('unexpected token "' + this.peek().type + '"'); 2901 | } 2902 | }, 2903 | 2904 | /** 2905 | * Text 2906 | */ 2907 | 2908 | parseText: function(){ 2909 | var tok = this.expect('text') 2910 | , node = new nodes.Text(tok.val); 2911 | node.line = this.line(); 2912 | return node; 2913 | }, 2914 | 2915 | /** 2916 | * ':' expr 2917 | * | block 2918 | */ 2919 | 2920 | parseBlockExpansion: function(){ 2921 | if (':' == this.peek().type) { 2922 | this.advance(); 2923 | return new nodes.Block(this.parseExpr()); 2924 | } else { 2925 | return this.block(); 2926 | } 2927 | }, 2928 | 2929 | /** 2930 | * case 2931 | */ 2932 | 2933 | parseCase: function(){ 2934 | var val = this.expect('case').val 2935 | , node = new nodes.Case(val); 2936 | node.line = this.line(); 2937 | node.block = this.block(); 2938 | return node; 2939 | }, 2940 | 2941 | /** 2942 | * when 2943 | */ 2944 | 2945 | parseWhen: function(){ 2946 | var val = this.expect('when').val 2947 | return new nodes.Case.When(val, this.parseBlockExpansion()); 2948 | }, 2949 | 2950 | /** 2951 | * default 2952 | */ 2953 | 2954 | parseDefault: function(){ 2955 | this.expect('default'); 2956 | return new nodes.Case.When('default', this.parseBlockExpansion()); 2957 | }, 2958 | 2959 | /** 2960 | * code 2961 | */ 2962 | 2963 | parseCode: function(){ 2964 | var tok = this.expect('code') 2965 | , node = new nodes.Code(tok.val, tok.buffer, tok.escape) 2966 | , block 2967 | , i = 1; 2968 | node.line = this.line(); 2969 | while (this.lookahead(i) && 'newline' == this.lookahead(i).type) ++i; 2970 | block = 'indent' == this.lookahead(i).type; 2971 | if (block) { 2972 | this.skip(i-1); 2973 | node.block = this.block(); 2974 | } 2975 | return node; 2976 | }, 2977 | 2978 | /** 2979 | * comment 2980 | */ 2981 | 2982 | parseComment: function(){ 2983 | var tok = this.expect('comment') 2984 | , node; 2985 | 2986 | if ('indent' == this.peek().type) { 2987 | node = new nodes.BlockComment(tok.val, this.block(), tok.buffer); 2988 | } else { 2989 | node = new nodes.Comment(tok.val, tok.buffer); 2990 | } 2991 | 2992 | node.line = this.line(); 2993 | return node; 2994 | }, 2995 | 2996 | /** 2997 | * doctype 2998 | */ 2999 | 3000 | parseDoctype: function(){ 3001 | var tok = this.expect('doctype') 3002 | , node = new nodes.Doctype(tok.val); 3003 | node.line = this.line(); 3004 | return node; 3005 | }, 3006 | 3007 | /** 3008 | * filter attrs? text-block 3009 | */ 3010 | 3011 | parseFilter: function(){ 3012 | var block 3013 | , tok = this.expect('filter') 3014 | , attrs = this.accept('attrs'); 3015 | 3016 | this.lexer.pipeless = true; 3017 | block = this.parseTextBlock(); 3018 | this.lexer.pipeless = false; 3019 | 3020 | var node = new nodes.Filter(tok.val, block, attrs && attrs.attrs); 3021 | node.line = this.line(); 3022 | return node; 3023 | }, 3024 | 3025 | /** 3026 | * tag ':' attrs? block 3027 | */ 3028 | 3029 | parseASTFilter: function(){ 3030 | var block 3031 | , tok = this.expect('tag') 3032 | , attrs = this.accept('attrs'); 3033 | 3034 | this.expect(':'); 3035 | block = this.block(); 3036 | 3037 | var node = new nodes.Filter(tok.val, block, attrs && attrs.attrs); 3038 | node.line = this.line(); 3039 | return node; 3040 | }, 3041 | 3042 | /** 3043 | * each block 3044 | */ 3045 | 3046 | parseEach: function(){ 3047 | var tok = this.expect('each') 3048 | , node = new nodes.Each(tok.code, tok.val, tok.key); 3049 | node.line = this.line(); 3050 | node.block = this.block(); 3051 | if (this.peek().type == 'code' && this.peek().val == 'else') { 3052 | this.advance(); 3053 | node.alternative = this.block(); 3054 | } 3055 | return node; 3056 | }, 3057 | 3058 | /** 3059 | * 'extends' name 3060 | */ 3061 | 3062 | parseExtends: function(){ 3063 | var path = require('path') 3064 | , fs = require('fs') 3065 | , dirname = path.dirname 3066 | , basename = path.basename 3067 | , join = path.join; 3068 | 3069 | if (!this.filename) 3070 | throw new Error('the "filename" option is required to extend templates'); 3071 | 3072 | var path = this.expect('extends').val.trim() 3073 | , dir = dirname(this.filename); 3074 | 3075 | var path = join(dir, path + '.jade') 3076 | , str = fs.readFileSync(path, 'utf8') 3077 | , parser = new Parser(str, path, this.options); 3078 | 3079 | parser.blocks = this.blocks; 3080 | parser.contexts = this.contexts; 3081 | this.extending = parser; 3082 | 3083 | // TODO: null node 3084 | return new nodes.Literal(''); 3085 | }, 3086 | 3087 | /** 3088 | * 'block' name block 3089 | */ 3090 | 3091 | parseBlock: function(){ 3092 | var block = this.expect('block') 3093 | , mode = block.mode 3094 | , name = block.val.trim(); 3095 | 3096 | block = 'indent' == this.peek().type 3097 | ? this.block() 3098 | : new nodes.Block(new nodes.Literal('')); 3099 | 3100 | var prev = this.blocks[name]; 3101 | 3102 | if (prev) { 3103 | switch (prev.mode) { 3104 | case 'append': 3105 | block.nodes = block.nodes.concat(prev.nodes); 3106 | prev = block; 3107 | break; 3108 | case 'prepend': 3109 | block.nodes = prev.nodes.concat(block.nodes); 3110 | prev = block; 3111 | break; 3112 | } 3113 | } 3114 | 3115 | block.mode = mode; 3116 | return this.blocks[name] = prev || block; 3117 | }, 3118 | 3119 | /** 3120 | * include block? 3121 | */ 3122 | 3123 | parseInclude: function(){ 3124 | var path = require('path') 3125 | , fs = require('fs') 3126 | , dirname = path.dirname 3127 | , basename = path.basename 3128 | , join = path.join; 3129 | 3130 | var path = this.expect('include').val.trim() 3131 | , dir = dirname(this.filename); 3132 | 3133 | if (!this.filename) 3134 | throw new Error('the "filename" option is required to use includes'); 3135 | 3136 | // no extension 3137 | if (!~basename(path).indexOf('.')) { 3138 | path += '.jade'; 3139 | } 3140 | 3141 | // non-jade 3142 | if ('.jade' != path.substr(-5)) { 3143 | var path = join(dir, path) 3144 | , str = fs.readFileSync(path, 'utf8'); 3145 | return new nodes.Literal(str); 3146 | } 3147 | 3148 | var path = join(dir, path) 3149 | , str = fs.readFileSync(path, 'utf8') 3150 | , parser = new Parser(str, path, this.options); 3151 | parser.blocks = utils.merge({}, this.blocks); 3152 | parser.mixins = this.mixins; 3153 | 3154 | this.context(parser); 3155 | var ast = parser.parse(); 3156 | this.context(); 3157 | ast.filename = path; 3158 | 3159 | if ('indent' == this.peek().type) { 3160 | ast.includeBlock().push(this.block()); 3161 | } 3162 | 3163 | return ast; 3164 | }, 3165 | 3166 | /** 3167 | * call ident block 3168 | */ 3169 | 3170 | parseCall: function(){ 3171 | var tok = this.expect('call') 3172 | , name = tok.val 3173 | , args = tok.args 3174 | , mixin = new nodes.Mixin(name, args, new nodes.Block, true); 3175 | 3176 | this.tag(mixin); 3177 | if (mixin.block.isEmpty()) mixin.block = null; 3178 | return mixin; 3179 | }, 3180 | 3181 | /** 3182 | * mixin block 3183 | */ 3184 | 3185 | parseMixin: function(){ 3186 | var tok = this.expect('mixin') 3187 | , name = tok.val 3188 | , args = tok.args 3189 | , mixin; 3190 | 3191 | // definition 3192 | if ('indent' == this.peek().type) { 3193 | mixin = new nodes.Mixin(name, args, this.block(), false); 3194 | this.mixins[name] = mixin; 3195 | return mixin; 3196 | // call 3197 | } else { 3198 | return new nodes.Mixin(name, args, null, true); 3199 | } 3200 | }, 3201 | 3202 | /** 3203 | * indent (text | newline)* outdent 3204 | */ 3205 | 3206 | parseTextBlock: function(){ 3207 | var block = new nodes.Block; 3208 | block.line = this.line(); 3209 | var spaces = this.expect('indent').val; 3210 | if (null == this._spaces) this._spaces = spaces; 3211 | var indent = Array(spaces - this._spaces + 1).join(' '); 3212 | while ('outdent' != this.peek().type) { 3213 | switch (this.peek().type) { 3214 | case 'newline': 3215 | this.advance(); 3216 | break; 3217 | case 'indent': 3218 | this.parseTextBlock().nodes.forEach(function(node){ 3219 | block.push(node); 3220 | }); 3221 | break; 3222 | default: 3223 | var text = new nodes.Text(indent + this.advance().val); 3224 | text.line = this.line(); 3225 | block.push(text); 3226 | } 3227 | } 3228 | 3229 | if (spaces == this._spaces) this._spaces = null; 3230 | this.expect('outdent'); 3231 | return block; 3232 | }, 3233 | 3234 | /** 3235 | * indent expr* outdent 3236 | */ 3237 | 3238 | block: function(){ 3239 | var block = new nodes.Block; 3240 | block.line = this.line(); 3241 | this.expect('indent'); 3242 | while ('outdent' != this.peek().type) { 3243 | if ('newline' == this.peek().type) { 3244 | this.advance(); 3245 | } else { 3246 | block.push(this.parseExpr()); 3247 | } 3248 | } 3249 | this.expect('outdent'); 3250 | return block; 3251 | }, 3252 | 3253 | /** 3254 | * interpolation (attrs | class | id)* (text | code | ':')? newline* block? 3255 | */ 3256 | 3257 | parseInterpolation: function(){ 3258 | var tok = this.advance(); 3259 | var tag = new nodes.Tag(tok.val); 3260 | tag.buffer = true; 3261 | return this.tag(tag); 3262 | }, 3263 | 3264 | /** 3265 | * tag (attrs | class | id)* (text | code | ':')? newline* block? 3266 | */ 3267 | 3268 | parseTag: function(){ 3269 | // ast-filter look-ahead 3270 | var i = 2; 3271 | if ('attrs' == this.lookahead(i).type) ++i; 3272 | if (':' == this.lookahead(i).type) { 3273 | if ('indent' == this.lookahead(++i).type) { 3274 | return this.parseASTFilter(); 3275 | } 3276 | } 3277 | 3278 | var tok = this.advance() 3279 | , tag = new nodes.Tag(tok.val); 3280 | 3281 | tag.selfClosing = tok.selfClosing; 3282 | 3283 | return this.tag(tag); 3284 | }, 3285 | 3286 | /** 3287 | * Parse tag. 3288 | */ 3289 | 3290 | tag: function(tag){ 3291 | var dot; 3292 | 3293 | tag.line = this.line(); 3294 | 3295 | // (attrs | class | id)* 3296 | out: 3297 | while (true) { 3298 | switch (this.peek().type) { 3299 | case 'id': 3300 | case 'class': 3301 | var tok = this.advance(); 3302 | tag.setAttribute(tok.type, "'" + tok.val + "'"); 3303 | continue; 3304 | case 'attrs': 3305 | var tok = this.advance() 3306 | , obj = tok.attrs 3307 | , escaped = tok.escaped 3308 | , names = Object.keys(obj); 3309 | 3310 | if (tok.selfClosing) tag.selfClosing = true; 3311 | 3312 | for (var i = 0, len = names.length; i < len; ++i) { 3313 | var name = names[i] 3314 | , val = obj[name]; 3315 | tag.setAttribute(name, val, escaped[name]); 3316 | } 3317 | continue; 3318 | default: 3319 | break out; 3320 | } 3321 | } 3322 | 3323 | // check immediate '.' 3324 | if ('.' == this.peek().val) { 3325 | dot = tag.textOnly = true; 3326 | this.advance(); 3327 | } 3328 | 3329 | // (text | code | ':')? 3330 | switch (this.peek().type) { 3331 | case 'text': 3332 | tag.block.push(this.parseText()); 3333 | break; 3334 | case 'code': 3335 | tag.code = this.parseCode(); 3336 | break; 3337 | case ':': 3338 | this.advance(); 3339 | tag.block = new nodes.Block; 3340 | tag.block.push(this.parseExpr()); 3341 | break; 3342 | } 3343 | 3344 | // newline* 3345 | while ('newline' == this.peek().type) this.advance(); 3346 | 3347 | tag.textOnly = tag.textOnly || ~textOnly.indexOf(tag.name); 3348 | 3349 | // script special-case 3350 | if ('script' == tag.name) { 3351 | var type = tag.getAttribute('type'); 3352 | if (!dot && type && 'text/javascript' != type.replace(/^['"]|['"]$/g, '')) { 3353 | tag.textOnly = false; 3354 | } 3355 | } 3356 | 3357 | // block? 3358 | if ('indent' == this.peek().type) { 3359 | if (tag.textOnly) { 3360 | this.lexer.pipeless = true; 3361 | tag.block = this.parseTextBlock(); 3362 | this.lexer.pipeless = false; 3363 | } else { 3364 | var block = this.block(); 3365 | if (tag.block) { 3366 | for (var i = 0, len = block.nodes.length; i < len; ++i) { 3367 | tag.block.push(block.nodes[i]); 3368 | } 3369 | } else { 3370 | tag.block = block; 3371 | } 3372 | } 3373 | } 3374 | 3375 | return tag; 3376 | } 3377 | }; 3378 | 3379 | }); // module: parser.js 3380 | 3381 | require.register("runtime.js", function(module, exports, require){ 3382 | 3383 | /*! 3384 | * Jade - runtime 3385 | * Copyright(c) 2010 TJ Holowaychuk 3386 | * MIT Licensed 3387 | */ 3388 | 3389 | /** 3390 | * Lame Array.isArray() polyfill for now. 3391 | */ 3392 | 3393 | if (!Array.isArray) { 3394 | Array.isArray = function(arr){ 3395 | return '[object Array]' == Object.prototype.toString.call(arr); 3396 | }; 3397 | } 3398 | 3399 | /** 3400 | * Lame Object.keys() polyfill for now. 3401 | */ 3402 | 3403 | if (!Object.keys) { 3404 | Object.keys = function(obj){ 3405 | var arr = []; 3406 | for (var key in obj) { 3407 | if (obj.hasOwnProperty(key)) { 3408 | arr.push(key); 3409 | } 3410 | } 3411 | return arr; 3412 | } 3413 | } 3414 | 3415 | /** 3416 | * Merge two attribute objects giving precedence 3417 | * to values in object `b`. Classes are special-cased 3418 | * allowing for arrays and merging/joining appropriately 3419 | * resulting in a string. 3420 | * 3421 | * @param {Object} a 3422 | * @param {Object} b 3423 | * @return {Object} a 3424 | * @api private 3425 | */ 3426 | 3427 | exports.merge = function merge(a, b) { 3428 | var ac = a['class']; 3429 | var bc = b['class']; 3430 | 3431 | if (ac || bc) { 3432 | ac = ac || []; 3433 | bc = bc || []; 3434 | if (!Array.isArray(ac)) ac = [ac]; 3435 | if (!Array.isArray(bc)) bc = [bc]; 3436 | ac = ac.filter(nulls); 3437 | bc = bc.filter(nulls); 3438 | a['class'] = ac.concat(bc).join(' '); 3439 | } 3440 | 3441 | for (var key in b) { 3442 | if (key != 'class') { 3443 | a[key] = b[key]; 3444 | } 3445 | } 3446 | 3447 | return a; 3448 | }; 3449 | 3450 | /** 3451 | * Filter null `val`s. 3452 | * 3453 | * @param {Mixed} val 3454 | * @return {Mixed} 3455 | * @api private 3456 | */ 3457 | 3458 | function nulls(val) { 3459 | return val != null; 3460 | } 3461 | 3462 | /** 3463 | * Render the given attributes object. 3464 | * 3465 | * @param {Object} obj 3466 | * @param {Object} escaped 3467 | * @return {String} 3468 | * @api private 3469 | */ 3470 | 3471 | exports.attrs = function attrs(obj, escaped){ 3472 | var buf = [] 3473 | , terse = obj.terse; 3474 | 3475 | delete obj.terse; 3476 | var keys = Object.keys(obj) 3477 | , len = keys.length; 3478 | 3479 | if (len) { 3480 | buf.push(''); 3481 | for (var i = 0; i < len; ++i) { 3482 | var key = keys[i] 3483 | , val = obj[key]; 3484 | 3485 | if ('boolean' == typeof val || null == val) { 3486 | if (val) { 3487 | terse 3488 | ? buf.push(key) 3489 | : buf.push(key + '="' + key + '"'); 3490 | } 3491 | } else if (0 == key.indexOf('data') && 'string' != typeof val) { 3492 | buf.push(key + "='" + JSON.stringify(val) + "'"); 3493 | } else if ('class' == key && Array.isArray(val)) { 3494 | buf.push(key + '="' + exports.escape(val.join(' ')) + '"'); 3495 | } else if (escaped && escaped[key]) { 3496 | buf.push(key + '="' + exports.escape(val) + '"'); 3497 | } else { 3498 | buf.push(key + '="' + val + '"'); 3499 | } 3500 | } 3501 | } 3502 | 3503 | return buf.join(' '); 3504 | }; 3505 | 3506 | /** 3507 | * Escape the given string of `html`. 3508 | * 3509 | * @param {String} html 3510 | * @return {String} 3511 | * @api private 3512 | */ 3513 | 3514 | exports.escape = function escape(html){ 3515 | return String(html) 3516 | .replace(/&(?!(\w+|\#\d+);)/g, '&') 3517 | .replace(//g, '>') 3519 | .replace(/"/g, '"'); 3520 | }; 3521 | 3522 | /** 3523 | * Re-throw the given `err` in context to the 3524 | * the jade in `filename` at the given `lineno`. 3525 | * 3526 | * @param {Error} err 3527 | * @param {String} filename 3528 | * @param {String} lineno 3529 | * @api private 3530 | */ 3531 | 3532 | exports.rethrow = function rethrow(err, filename, lineno){ 3533 | if (!filename) throw err; 3534 | 3535 | var context = 3 3536 | , str = require('fs').readFileSync(filename, 'utf8') 3537 | , lines = str.split('\n') 3538 | , start = Math.max(lineno - context, 0) 3539 | , end = Math.min(lines.length, lineno + context); 3540 | 3541 | // Error context 3542 | var context = lines.slice(start, end).map(function(line, i){ 3543 | var curr = i + start + 1; 3544 | return (curr == lineno ? ' > ' : ' ') 3545 | + curr 3546 | + '| ' 3547 | + line; 3548 | }).join('\n'); 3549 | 3550 | // Alter exception message 3551 | err.path = filename; 3552 | err.message = (filename || 'Jade') + ':' + lineno 3553 | + '\n' + context + '\n\n' + err.message; 3554 | throw err; 3555 | }; 3556 | 3557 | }); // module: runtime.js 3558 | 3559 | require.register("self-closing.js", function(module, exports, require){ 3560 | 3561 | /*! 3562 | * Jade - self closing tags 3563 | * Copyright(c) 2010 TJ Holowaychuk 3564 | * MIT Licensed 3565 | */ 3566 | 3567 | module.exports = [ 3568 | 'meta' 3569 | , 'img' 3570 | , 'link' 3571 | , 'input' 3572 | , 'source' 3573 | , 'area' 3574 | , 'base' 3575 | , 'col' 3576 | , 'br' 3577 | , 'hr' 3578 | ]; 3579 | }); // module: self-closing.js 3580 | 3581 | require.register("utils.js", function(module, exports, require){ 3582 | 3583 | /*! 3584 | * Jade - utils 3585 | * Copyright(c) 2010 TJ Holowaychuk 3586 | * MIT Licensed 3587 | */ 3588 | 3589 | /** 3590 | * Convert interpolation in the given string to JavaScript. 3591 | * 3592 | * @param {String} str 3593 | * @return {String} 3594 | * @api private 3595 | */ 3596 | 3597 | var interpolate = exports.interpolate = function(str){ 3598 | return str.replace(/(_SLASH_)?([#!]){(.*?)}/g, function(str, escape, flag, code){ 3599 | code = code 3600 | .replace(/\\'/g, "'") 3601 | .replace(/_SLASH_/g, '\\'); 3602 | 3603 | return escape 3604 | ? str.slice(7) 3605 | : "' + " 3606 | + ('!' == flag ? '' : 'escape') 3607 | + "((interp = " + code 3608 | + ") == null ? '' : interp) + '"; 3609 | }); 3610 | }; 3611 | 3612 | /** 3613 | * Escape single quotes in `str`. 3614 | * 3615 | * @param {String} str 3616 | * @return {String} 3617 | * @api private 3618 | */ 3619 | 3620 | var escape = exports.escape = function(str) { 3621 | return str.replace(/'/g, "\\'"); 3622 | }; 3623 | 3624 | /** 3625 | * Interpolate, and escape the given `str`. 3626 | * 3627 | * @param {String} str 3628 | * @return {String} 3629 | * @api private 3630 | */ 3631 | 3632 | exports.text = function(str){ 3633 | return interpolate(escape(str)); 3634 | }; 3635 | 3636 | /** 3637 | * Merge `b` into `a`. 3638 | * 3639 | * @param {Object} a 3640 | * @param {Object} b 3641 | * @return {Object} 3642 | * @api public 3643 | */ 3644 | 3645 | exports.merge = function(a, b) { 3646 | for (var key in b) a[key] = b[key]; 3647 | return a; 3648 | }; 3649 | 3650 | 3651 | }); // module: utils.js 3652 | 3653 | window.jade = require("jade"); 3654 | })(); 3655 | --------------------------------------------------------------------------------