rendered correctly a simple and default LocomotiveCMS site
did
committed Dec 30, 2012
commit c95649ad22594342e28d009472c7c04a6da96d98
Showing 48
changed files with
1870 additions
and 62 deletions
.gitignore
+3
-0
| @@ | @@ -16,3 +16,6 @@ spec/reports |
| test/tmp | |
| test/version_tmp | |
| tmp | |
| + | |
| + | /.rbenv-gemsets |
| + | /.sass-cache/ |
Gemfile
+2
-1
| @@ | @@ -2,4 +2,5 @@ source 'https://rubygems.org' |
| # Specify your gem's dependencies in steam.gemspec | |
| gemspec | |
| - | gem 'locomotivecms_mounter', path: '../mounter' |
| \ No newline at end of file | |
| + | |
| + | gem 'locomotivecms_mounter', path: '../gems/mounter' |
| \ No newline at end of file | |
bin/builder
+4
-0
| @@ | @@ -1,4 +1,8 @@ |
| #!/usr/bin/env ruby | |
| + | |
| + | # FIXME: needed if you don't launch it with bundler |
| + | # $:.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib')) |
| + | |
| require "locomotive/builder" | |
| require "locomotive/builder/cli" | |
locomotive/builder/cli.rb b/lib/locomotive/builder/cli.rb
+14
-3
| @@ | @@ -3,20 +3,31 @@ require "thor" |
| module Locomotive | |
| module Builder | |
| class CLI < Thor | |
| - | |
| + | |
| desc "import NAME SITE_URL EMAIL PASSWORD", "Import an existing locomotive site" | |
| def import(name, site_url, email, password) | |
| say("ERROR: \"#{name}\" directory already exists", :red) and return if File.exists?(name) | |
| Locomotive::Builder.import(name, site_url, email, password) | |
| end | |
| - | |
| + | |
| + | desc "push PATH SITE_URL EMAIL PASSWORD", "Push a site created by the builder to a remote LocomotiveCMS engine" |
| + | def push(path, site_url, email, password) |
| + | Locomotive::Builder.push(path, site_url, email, password) |
| + | end |
| + | |
| desc "server PATH [PORT]", "Serve an existing site from the file system" | |
| def server(path, port = 3333) | |
| require "thin" | |
| require "locomotive/builder/server" | |
| reader = Locomotive::Mounter::Reader::FileSystem.instance | |
| reader.run!(path: path) | |
| - | Thin::Server.start('0.0.0.0', port, Locomotive::Builder::Server.new(reader)) |
| + | |
| + | server = Thin::Server.new('0.0.0.0', port, Locomotive::Builder::Server.new(reader)) |
| + | server.threaded = true |
| + | server.start |
| + | |
| + | # TODO: To be removed |
| + | # Thin::Server.start('0.0.0.0', port, Locomotive::Builder::Server.new(reader), threaded: true) |
| end | |
| end | |
| end | |
locomotive/builder/liquid.rb b/lib/locomotive/builder/liquid.rb
+21
-0
| @@ | @@ -0,0 +1,21 @@ |
| + | require 'liquid' |
| + | require 'locomotive/builder/liquid/drops/base' |
| + | |
| + | %w{. drops tags filters}.each do |dir| |
| + | Dir[File.join(File.dirname(__FILE__), 'liquid', dir, '*.rb')].each { |lib| require lib } |
| + | end |
| + | |
| + | # add to_liquid methods to main models from the mounter |
| + | %w{site page content_entry}.each do |name| |
| + | # require "locomotive/mounter/models/#{name}" |
| + | |
| + | klass = "Locomotive::Mounter::Models::#{name.classify}".constantize |
| + | |
| + | klass.class_eval <<-EOV |
| + | def to_liquid |
| + | ::Locomotive::Builder::Liquid::Drops::#{name.classify}.new(self) |
| + | end |
| + | EOV |
| + | end |
| + | |
| + | # ::Liquid::Template.file_system = Locomotive::Builder::Liquid::TemplateFileSystem.new(LocomotiveEditor.site_templates_root) |
locomotive/builder/liquid/drops/base.rb b/lib/locomotive/builder/liquid/drops/base.rb
+44
-0
| @@ | @@ -0,0 +1,44 @@ |
| + | # Code taken from Mephisto sources (http://mephistoblog.com/) |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Drops |
| + | class Base < ::Liquid::Drop |
| + | |
| + | @@forbidden_attributes = %w{_id _version _index} |
| + | |
| + | attr_reader :_source |
| + | |
| + | def initialize(source) |
| + | @_source = source |
| + | end |
| + | |
| + | def id |
| + | (@_source.respond_to?(:id) ? @_source.id : nil) || 'new' |
| + | end |
| + | |
| + | # converts an array of records to an array of liquid drops |
| + | def self.liquify(*records, &block) |
| + | i = -1 |
| + | records = |
| + | records.inject [] do |all, r| |
| + | i+=1 |
| + | attrs = (block && block.arity == 1) ? [r] : [r, i] |
| + | all << (block ? block.call(*attrs) : r.to_liquid) |
| + | all |
| + | end |
| + | records.compact! |
| + | records |
| + | end |
| + | |
| + | protected |
| + | |
| + | def liquify(*records, &block) |
| + | self.class.liquify(*records, &block) |
| + | end |
| + | |
| + | end |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/drops/content_entry.rb b/lib/locomotive/builder/liquid/drops/content_entry.rb
+29
-0
| @@ | @@ -0,0 +1,29 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Drops |
| + | class ContentEntry < Base |
| + | |
| + | delegate :_permalink, :seo_title, :meta_keywords, :meta_description, :to => '_source' |
| + | |
| + | def before_method(meth) |
| + | return '' if @_source.nil? |
| + | |
| + | if not @@forbidden_attributes.include?(meth.to_s) |
| + | value = @_source.send(meth) |
| + | end |
| + | end |
| + | |
| + | def _permalink |
| + | @_source._permalink.parameterize |
| + | end |
| + | |
| + | def highlighted_field_value |
| + | @_source.highlighted_field_value |
| + | end |
| + | |
| + | end |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/drops/content_types.rb b/lib/locomotive/builder/liquid/drops/content_types.rb
+103
-0
| @@ | @@ -0,0 +1,103 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Drops |
| + | class ContentTypes < ::Liquid::Drop |
| + | |
| + | def before_method(meth) |
| + | type = self.mounting_point.content_types[meth.to_s] |
| + | ProxyCollection.new(type) |
| + | end |
| + | |
| + | end |
| + | |
| + | class ProxyCollection < ::Liquid::Drop |
| + | |
| + | def initialize(content_type) |
| + | @content_type = content_type |
| + | @collection = nil |
| + | end |
| + | |
| + | def first |
| + | self.collection.first |
| + | end |
| + | |
| + | def last |
| + | self.collection.last |
| + | end |
| + | |
| + | def size |
| + | self.collection.size |
| + | end |
| + | |
| + | alias :length :size |
| + | |
| + | def each(&block) |
| + | self.collection.each(&block) |
| + | end |
| + | |
| + | def public_submission_url |
| + | "/entry_submissions/#{@content_type.slug}" |
| + | end |
| + | |
| + | def api |
| + | { 'create' => "/entry_submissions/#{@content_type.slug}" } |
| + | end |
| + | |
| + | def before_method(meth) |
| + | if (meth.to_s =~ /^group_by_(.+)$/) == 0 |
| + | # TODO |
| + | @content_type.group_contents_by($1) |
| + | elsif (meth.to_s =~ /^(.+)_options$/) == 0 |
| + | # TODO |
| + | @content_type.select_names($1) |
| + | else |
| + | @content_type.send(meth) |
| + | end |
| + | end |
| + | |
| + | protected |
| + | |
| + | def paginate(options = {}) |
| + | @collection ||= self.collection.paginate(options) |
| + | { |
| + | collection: @collection, |
| + | current_page: @collection.current_page, |
| + | previous_page: @collection.previous_page, |
| + | next_page: @collection.next_page, |
| + | total_entries: @collection.total_entries, |
| + | total_pages: @collection.total_pages, |
| + | per_page: @collection.per_page |
| + | } |
| + | end |
| + | |
| + | def collection |
| + | return unless @collection.blank? |
| + | |
| + | if @context['with_scope'].blank? |
| + | @collection = @content_type.entries |
| + | else |
| + | @collection = [] |
| + | |
| + | conditions = @context['with_scope'].clone.delete_if { |k, _| %w(order_by per_page page).include?(k) } |
| + | |
| + | @content_type.contents.each do |content| |
| + | accepted = (conditions.map do |key, value| |
| + | case value |
| + | when TrueClass, FalseClass, String then content.send(key) == value |
| + | else |
| + | true |
| + | end |
| + | end).all? # all conditions works ? |
| + | |
| + | @collection << content if accepted |
| + | end |
| + | end |
| + | |
| + | @collection |
| + | end |
| + | end |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/drops/page.rb b/lib/locomotive/builder/liquid/drops/page.rb
+34
-0
| @@ | @@ -0,0 +1,34 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Drops |
| + | class Page < Base |
| + | |
| + | delegate :title, :slug, :fullpath, :parent, :depth, :seo_title, :redirect_url, :meta_description, :meta_keywords, :to => '_source' |
| + | |
| + | def children |
| + | @children ||= liquify(*@_source.children) |
| + | end |
| + | |
| + | def published? |
| + | @_source.published? |
| + | end |
| + | |
| + | def redirect? |
| + | self._source.redirect? |
| + | end |
| + | |
| + | def breadcrumbs |
| + | # TODO |
| + | '' |
| + | end |
| + | |
| + | def listed? |
| + | @_source.listed? |
| + | end |
| + | |
| + | end |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/drops/site.rb b/lib/locomotive/builder/liquid/drops/site.rb
+21
-0
| @@ | @@ -0,0 +1,21 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Drops |
| + | class Site < Base |
| + | |
| + | delegate :name, :seo_title, :meta_description, :meta_keywords, :to => '_source' |
| + | |
| + | def index |
| + | @index ||= @_source.lookup_page('index') |
| + | end |
| + | |
| + | def pages |
| + | @pages ||= liquify(*self._source.pages) |
| + | end |
| + | |
| + | end |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/errors.rb b/lib/locomotive/builder/liquid/errors.rb
+7
-0
| @@ | @@ -0,0 +1,7 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | class PageNotFound < ::Liquid::Error; end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/filters/date.rb b/lib/locomotive/builder/liquid/filters/date.rb
+98
-0
| @@ | @@ -0,0 +1,98 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Filters |
| + | module Date |
| + | |
| + | def localized_date(input, *args) |
| + | return '' if input.blank? |
| + | |
| + | format, locale = args |
| + | |
| + | locale ||= I18n.locale |
| + | format ||= I18n.t('date.formats.default', :locale => locale) |
| + | |
| + | if input.is_a?(String) |
| + | begin |
| + | fragments = ::Date._strptime(input, format) |
| + | input = ::Date.new(fragments[:year], fragments[:mon], fragments[:mday]) |
| + | rescue |
| + | input = Time.parse(input) |
| + | end |
| + | end |
| + | |
| + | return input.to_s unless input.respond_to?(:strftime) |
| + | |
| + | I18n.l input, :format => format, :locale => locale |
| + | end |
| + | |
| + | alias :format_date :localized_date |
| + | |
| + | def distance_of_time_in_words(input, *args) |
| + | return '' if input.blank? |
| + | |
| + | from_time = input |
| + | to_time = args[0] || Time.now |
| + | |
| + | from_time = from_time.to_time if from_time.respond_to?(:to_time) |
| + | to_time = to_time.to_time if to_time.respond_to?(:to_time) |
| + | distance_in_minutes = (((to_time - from_time).abs)/60).round |
| + | distance_in_seconds = ((to_time - from_time).abs).round |
| + | |
| + | ::I18n.with_options({ :scope => :'datetime.distance_in_words' }) do |locale| |
| + | |
| + | case distance_in_minutes |
| + | when 0..1 |
| + | return distance_in_minutes == 0 ? |
| + | locale.t(:less_than_x_minutes, :count => 1) : |
| + | locale.t(:x_minutes, :count => distance_in_minutes) unless include_seconds |
| + | |
| + | case distance_in_seconds |
| + | when 0..4 then locale.t :less_than_x_seconds, :count => 5 |
| + | when 5..9 then locale.t :less_than_x_seconds, :count => 10 |
| + | when 10..19 then locale.t :less_than_x_seconds, :count => 20 |
| + | when 20..39 then locale.t :half_a_minute |
| + | when 40..59 then locale.t :less_than_x_minutes, :count => 1 |
| + | else locale.t :x_minutes, :count => 1 |
| + | end |
| + | |
| + | when 2..44 then locale.t :x_minutes, :count => distance_in_minutes |
| + | when 45..89 then locale.t :about_x_hours, :count => 1 |
| + | when 90..1439 then locale.t :about_x_hours, :count => (distance_in_minutes.to_f / 60.0).round |
| + | when 1440..2519 then locale.t :x_days, :count => 1 |
| + | when 2520..43199 then locale.t :x_days, :count => (distance_in_minutes.to_f / 1440.0).round |
| + | when 43200..86399 then locale.t :about_x_months, :count => 1 |
| + | when 86400..525599 then locale.t :x_months, :count => (distance_in_minutes.to_f / 43200.0).round |
| + | else |
| + | fyear = from_time.year |
| + | fyear += 1 if from_time.month >= 3 |
| + | tyear = to_time.year |
| + | tyear -= 1 if to_time.month < 3 |
| + | leap_years = (fyear > tyear) ? 0 : (fyear..tyear).count{|x| ::Date.leap?(x)} |
| + | minute_offset_for_leap_year = leap_years * 1440 |
| + | # Discount the leap year days when calculating year distance. |
| + | # e.g. if there are 20 leap year days between 2 dates having the same day |
| + | # and month then the based on 365 days calculation |
| + | # the distance in years will come out to over 80 years when in written |
| + | # english it would read better as about 80 years. |
| + | minutes_with_offset = distance_in_minutes - minute_offset_for_leap_year |
| + | remainder = (minutes_with_offset % 525600) |
| + | distance_in_years = (minutes_with_offset / 525600) |
| + | if remainder < 131400 |
| + | locale.t(:about_x_years, :count => distance_in_years) |
| + | elsif remainder < 394200 |
| + | locale.t(:over_x_years, :count => distance_in_years) |
| + | else |
| + | locale.t(:almost_x_years, :count => distance_in_years + 1) |
| + | end |
| + | end |
| + | end |
| + | end |
| + | |
| + | end |
| + | |
| + | ::Liquid::Template.register_filter(Date) |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/filters/html.rb b/lib/locomotive/builder/liquid/filters/html.rb
+154
-0
| @@ | @@ -0,0 +1,154 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Filters |
| + | module Html |
| + | |
| + | # Write the link to a stylesheet resource |
| + | # input: url of the css file |
| + | def stylesheet_tag(input, media = 'screen') |
| + | return '' if input.nil? |
| + | |
| + | input = "/stylesheets/#{input}" unless input =~ /^(\/|http:)/ |
| + | |
| + | input = "#{input}.css" unless input.ends_with?('.css') |
| + | |
| + | %{<link href="#{input}" media="#{media}" rel="stylesheet" type="text/css" />} |
| + | end |
| + | |
| + | # Write the link to javascript resource |
| + | # input: url of the javascript file |
| + | def javascript_tag(input) |
| + | return '' if input.nil? |
| + | |
| + | input = "/javascripts/#{input}" unless input =~ /^(\/|http:)/ |
| + | |
| + | input = "#{input}.js" unless input.ends_with?('.js') |
| + | |
| + | %{<script src="#{input}" type="text/javascript"></script>} |
| + | end |
| + | |
| + | # Write an image tag |
| + | # input: url of the image OR asset drop |
| + | def image_tag(input, *args) |
| + | image_options = inline_options(args_to_options(args)) |
| + | |
| + | input = "/images/#{input}" unless input =~ /^(\/|http:)/ |
| + | |
| + | "<img src=\"#{File.join('/', get_url_from_asset(input))}\" #{image_options}/>" |
| + | end |
| + | |
| + | # Write a theme image tag |
| + | # input: name of file including folder |
| + | # example: 'about/myphoto.jpg' | theme_image # <img src="images/about/myphoto.jpg" /> |
| + | def theme_image_tag(input, *args) |
| + | image_options = inline_options(args_to_options(args)) |
| + | "<img src=\"#{theme_image_url(input)}\" #{image_options}/>" |
| + | end |
| + | |
| + | def theme_image_url(input) |
| + | return '' if input.nil? |
| + | |
| + | input = "images/#{input}" unless input.starts_with?('/') |
| + | |
| + | File.join('/', input) |
| + | end |
| + | |
| + | def image_format(input, *args) |
| + | format = args_to_options(args).first |
| + | "#{input}.#{format}" |
| + | end |
| + | |
| + | # Embed a flash movie into a page |
| + | # input: url of the flash movie OR asset drop |
| + | # width: width (in pixel or in %) of the embedded movie |
| + | # height: height (in pixel or in %) of the embedded movie |
| + | def flash_tag(input, *args) |
| + | path = get_url_from_asset(input) |
| + | embed_options = inline_options(args_to_options(args)) |
| + | %{ |
| + | <object #{embed_options}> |
| + | <param name="movie" value="#{path}" /> |
| + | <embed src="#{path}" #{embed_options}/> |
| + | </embed> |
| + | </object> |
| + | }.gsub(/ >/, '>').strip |
| + | end |
| + | |
| + | # Render the navigation for a paginated collection |
| + | def default_pagination(paginate, *args) |
| + | return '' if paginate['parts'].empty? |
| + | |
| + | options = args_to_options(args) |
| + | |
| + | previous_label = options[:previous_label] || I18n.t('pagination.previous') |
| + | next_label = options[:next_label] || I18n.t('pagination.next') |
| + | |
| + | previous_link = (if paginate['previous'].blank? |
| + | "<span class=\"disabled prev_page\">#{previous_label}</span>" |
| + | else |
| + | "<a href=\"#{absolute_url(paginate['previous']['url'])}\" class=\"prev_page\">#{previous_label}</a>" |
| + | end) |
| + | |
| + | links = "" |
| + | paginate['parts'].each do |part| |
| + | links << (if part['is_link'] |
| + | "<a href=\"#{absolute_url(part['url'])}\">#{part['title']}</a>" |
| + | elsif part['hellip_break'] |
| + | "<span class=\"gap\">#{part['title']}</span>" |
| + | else |
| + | "<span class=\"current\">#{part['title']}</span>" |
| + | end) |
| + | end |
| + | |
| + | next_link = (if paginate['next'].blank? |
| + | "<span class=\"disabled next_page\">#{next_label}</span>" |
| + | else |
| + | "<a href=\"#{absolute_url(paginate['next']['url'])}\" class=\"next_page\">#{next_label}</a>" |
| + | end) |
| + | |
| + | %{<div class="pagination #{options[:css]}"> |
| + | #{previous_link} |
| + | #{links} |
| + | #{next_link} |
| + | </div>} |
| + | end |
| + | |
| + | protected |
| + | |
| + | # Convert an array of properties ('key:value') into a hash |
| + | # Ex: ['width:50', 'height:100'] => { :width => '50', :height => '100' } |
| + | def args_to_options(*args) |
| + | options = {} |
| + | args.flatten.each do |a| |
| + | if (a =~ /^(.*):(.*)$/) |
| + | options[$1.to_sym] = $2 |
| + | end |
| + | end |
| + | options |
| + | end |
| + | |
| + | # Write options (Hash) into a string according to the following pattern: |
| + | # <key1>="<value1>", <key2>="<value2", ...etc |
| + | def inline_options(options = {}) |
| + | return '' if options.empty? |
| + | (options.stringify_keys.to_a.collect { |a, b| "#{a}=\"#{b}\"" }).join(' ') << ' ' |
| + | end |
| + | |
| + | # Get the path to be used in html tags such as image_tag, flash_tag, ...etc |
| + | # input: url (String) OR asset drop |
| + | def get_url_from_asset(input) |
| + | input.respond_to?(:url) ? input.url : input |
| + | end |
| + | |
| + | def absolute_url(url) |
| + | url =~ /^\// ? url : "/#{url}" |
| + | end |
| + | end |
| + | |
| + | ::Liquid::Template.register_filter(Html) |
| + | |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/filters/misc.rb b/lib/locomotive/builder/liquid/filters/misc.rb
+28
-0
| @@ | @@ -0,0 +1,28 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Filters |
| + | module Misc |
| + | |
| + | # was called modulo at first |
| + | def str_modulo(word, index, modulo) |
| + | (index.to_i + 1) % modulo == 0 ? word : '' |
| + | end |
| + | |
| + | # Get the nth element of the passed in array |
| + | def index(array, position) |
| + | array.at(position) if array.respond_to?(:at) |
| + | end |
| + | |
| + | def default(input, value) |
| + | input.blank? ? value : input |
| + | end |
| + | |
| + | end |
| + | |
| + | ::Liquid::Template.register_filter(Misc) |
| + | |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/filters/resize.rb b/lib/locomotive/builder/liquid/filters/resize.rb
+18
-0
| @@ | @@ -0,0 +1,18 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Filters |
| + | module Resize |
| + | |
| + | def resize(input, resize_string) |
| + | Locomotive::Builder::DragonflyExt.resize_url(input, resize_string) |
| + | end |
| + | |
| + | end |
| + | |
| + | ::Liquid::Template.register_filter(Resize) |
| + | |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/filters/text.rb b/lib/locomotive/builder/liquid/filters/text.rb
+50
-0
| @@ | @@ -0,0 +1,50 @@ |
| + | require 'RedCloth' |
| + | |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Filters |
| + | module Text |
| + | |
| + | # right justify and padd a string |
| + | def rjust(input, integer, padstr = '') |
| + | input.to_s.rjust(integer, padstr) |
| + | end |
| + | |
| + | # left justify and padd a string |
| + | def ljust(input, integer, padstr = '') |
| + | input.to_s.ljust(integer, padstr) |
| + | end |
| + | |
| + | def underscore(input) |
| + | input.to_s.gsub(' ', '_').gsub('/', '_').underscore |
| + | end |
| + | |
| + | def dasherize(input) |
| + | input.to_s.gsub(' ', '-').gsub('/', '-').dasherize |
| + | end |
| + | |
| + | # alias newline_to_br |
| + | def multi_line(input) |
| + | input.to_s.gsub("\n", '<br/>') |
| + | end |
| + | |
| + | def concat(input, *args) |
| + | result = input.to_s |
| + | args.flatten.each { |a| result << a.to_s } |
| + | result |
| + | end |
| + | |
| + | |
| + | def textile(input) |
| + | ::RedCloth.new(input).to_html |
| + | end |
| + | |
| + | end |
| + | |
| + | ::Liquid::Template.register_filter(Text) |
| + | |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/patches.rb b/lib/locomotive/builder/liquid/patches.rb
+47
-0
| @@ | @@ -0,0 +1,47 @@ |
| + | module Liquid |
| + | |
| + | class Drop |
| + | |
| + | def mounting_point |
| + | @context.registers[:mounting_point] |
| + | end |
| + | |
| + | def site |
| + | @context.registers[:site] |
| + | end |
| + | |
| + | end |
| + | |
| + | class Template |
| + | |
| + | # creates a new <tt>Template</tt> object from liquid source code |
| + | def parse_with_utf8(source, context = {}) |
| + | if RUBY_VERSION =~ /1\.9/ |
| + | source = source.force_encoding('UTF-8') if source.present? |
| + | end |
| + | self.parse_without_utf8(source, context) |
| + | end |
| + | |
| + | alias_method_chain :parse, :utf8 |
| + | |
| + | end |
| + | |
| + | module StandardFilters |
| + | |
| + | private |
| + | |
| + | def to_number(obj) |
| + | case obj |
| + | when Numeric |
| + | obj |
| + | when String |
| + | (obj.strip =~ /^\d+\.\d+$/) ? obj.to_f : obj.to_i |
| + | when DateTime, Date, Time |
| + | obj.to_time.to_i |
| + | else |
| + | 0 |
| + | end |
| + | end |
| + | end |
| + | |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/tags/consume.rb b/lib/locomotive/builder/liquid/tags/consume.rb
+58
-0
| @@ | @@ -0,0 +1,58 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Tags |
| + | |
| + | # Consume web services as easy as pie directly in liquid ! |
| + | # |
| + | # Usage: |
| + | # |
| + | # {% consume blog from 'http://nocoffee.tumblr.com/api/read.json?num=3' username: 'john', password: 'easy', format: 'json', expires_in: 3000 %} |
| + | # {% for post in blog.posts %} |
| + | # {{ post.title }} |
| + | # {% endfor %} |
| + | # {% endconsume %} |
| + | # |
| + | class Consume < ::Liquid::Block |
| + | |
| + | Syntax = /(#{::Liquid::VariableSignature}+)\s*from\s*(#{::Liquid::QuotedString}+)/ |
| + | |
| + | def initialize(tag_name, markup, tokens, context) |
| + | if markup =~ Syntax |
| + | @target = $1 |
| + | @url = $2.gsub(/['"]/, '') |
| + | @options = {} |
| + | markup.scan(::Liquid::TagAttributes) do |key, value| |
| + | @options[key] = value if key != 'http' |
| + | end |
| + | @options.delete('expires_in') |
| + | else |
| + | raise ::Liquid::SyntaxError.new("Syntax Error in 'consume' - Valid syntax: consume <var> from \"<url>\" [username: value, password: value]") |
| + | end |
| + | |
| + | super |
| + | end |
| + | |
| + | def render(context) |
| + | context.stack do |
| + | _response = nil |
| + | |
| + | begin |
| + | _response = Locomotive::Builder::Httparty::Webservice.consume(@url, @options.symbolize_keys) |
| + | rescue Exception => e |
| + | _response = { 'error' => e.message.to_s }.to_liquid |
| + | end |
| + | |
| + | context.scopes.last[@target.to_s] = _response |
| + | |
| + | render_all(@nodelist, context) |
| + | end |
| + | end |
| + | |
| + | end |
| + | |
| + | ::Liquid::Template.register_tag('consume', Consume) |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/tags/csrf.rb b/lib/locomotive/builder/liquid/tags/csrf.rb
+34
-0
| @@ | @@ -0,0 +1,34 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Tags |
| + | module Csrf |
| + | |
| + | class Param < ::Liquid::Tag |
| + | |
| + | def render(context) |
| + | %{<input type="hidden" name="authenticity_token" value="helloworld" />} |
| + | end |
| + | |
| + | end |
| + | |
| + | class Meta < ::Liquid::Tag |
| + | |
| + | def render(context) |
| + | %{ |
| + | <meta name="csrf-param" content="authenticity_token" /> |
| + | <meta name="csrf-token" content="helloworld" /> |
| + | } |
| + | end |
| + | |
| + | end |
| + | |
| + | end |
| + | |
| + | ::Liquid::Template.register_tag('csrf_param', Csrf::Param) |
| + | ::Liquid::Template.register_tag('csrf_meta', Csrf::Meta) |
| + | |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/tags/editable.rb b/lib/locomotive/builder/liquid/tags/editable.rb
+5
-0
| @@ | @@ -0,0 +1,5 @@ |
| + | require 'locomotive/builder/liquid/tags/editable/base' |
| + | require 'locomotive/builder/liquid/tags/editable/short_text' |
| + | require 'locomotive/builder/liquid/tags/editable/long_text' |
| + | require 'locomotive/builder/liquid/tags/editable/file' |
| + | require 'locomotive/builder/liquid/tags/editable/control' |
| \ No newline at end of file | |
locomotive/builder/liquid/tags/editable/base.rb b/lib/locomotive/builder/liquid/tags/editable/base.rb
+32
-0
| @@ | @@ -0,0 +1,32 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Tags |
| + | module Editable |
| + | class Base < ::Liquid::Block |
| + | |
| + | Syntax = /(#{::Liquid::QuotedFragment})(\s*,\s*#{::Liquid::Expression}+)?/ |
| + | |
| + | def initialize(tag_name, markup, tokens, context) |
| + | if markup =~ Syntax |
| + | @slug = $1.gsub('\'', '') |
| + | @options = {} |
| + | markup.scan(::Liquid::TagAttributes) { |key, value| @options[key.to_sym] = value.gsub(/^'/, '').gsub(/'$/, '') } |
| + | else |
| + | raise ::Liquid::SyntaxError.new("Syntax Error in 'editable_xxx' - Valid syntax: editable_xxx <slug>(, <options>)") |
| + | end |
| + | |
| + | super |
| + | end |
| + | |
| + | def render(context) |
| + | super |
| + | end |
| + | |
| + | end |
| + | |
| + | end |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/tags/editable/control.rb b/lib/locomotive/builder/liquid/tags/editable/control.rb
+15
-0
| @@ | @@ -0,0 +1,15 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Tags |
| + | module Editable |
| + | class Control < Base |
| + | |
| + | end |
| + | |
| + | ::Liquid::Template.register_tag('editable_control', Control) |
| + | end |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/tags/editable/file.rb b/lib/locomotive/builder/liquid/tags/editable/file.rb
+15
-0
| @@ | @@ -0,0 +1,15 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Tags |
| + | module Editable |
| + | class File < Base |
| + | |
| + | end |
| + | |
| + | ::Liquid::Template.register_tag('editable_file', File) |
| + | end |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/tags/editable/long_text.rb b/lib/locomotive/builder/liquid/tags/editable/long_text.rb
+15
-0
| @@ | @@ -0,0 +1,15 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Tags |
| + | module Editable |
| + | class LongText < ShortText |
| + | |
| + | end |
| + | |
| + | ::Liquid::Template.register_tag('editable_long_text', LongText) |
| + | end |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/tags/editable/short_text.rb b/lib/locomotive/builder/liquid/tags/editable/short_text.rb
+15
-0
| @@ | @@ -0,0 +1,15 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Tags |
| + | module Editable |
| + | class ShortText < Base |
| + | |
| + | end |
| + | |
| + | ::Liquid::Template.register_tag('editable_short_text', ShortText) |
| + | end |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/tags/extends.rb b/lib/locomotive/builder/liquid/tags/extends.rb
+25
-0
| @@ | @@ -0,0 +1,25 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Tags |
| + | class Extends < ::Liquid::Extends |
| + | |
| + | def parse_parent_template |
| + | mounting_point = @context[:mounting_point] |
| + | |
| + | page = if @template_name == 'parent' |
| + | @context[:page].parent |
| + | else |
| + | mounting_point.pages[@template_name] |
| + | end |
| + | |
| + | ::Liquid::Template.parse(page.source, { mounting_point: mounting_point, page: page }) |
| + | end |
| + | |
| + | end |
| + | |
| + | ::Liquid::Template.register_tag('extends', Extends) |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/tags/google_analytics.rb b/lib/locomotive/builder/liquid/tags/google_analytics.rb
+28
-0
| @@ | @@ -0,0 +1,28 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Tags |
| + | class GoogleAnalytics < ::Liquid::Tag |
| + | |
| + | Syntax = /(#{::Liquid::Expression}+)?/ |
| + | |
| + | def initialize(tag_name, markup, tokens, context) |
| + | if markup =~ Syntax |
| + | @account_id = $1.gsub('\'', '') |
| + | else |
| + | raise ::Liquid::SyntaxError.new("Syntax Error in 'google_analytics' - Valid syntax: google_analytics <account_id>") |
| + | end |
| + | |
| + | super |
| + | end |
| + | |
| + | def render(context) |
| + | "<!-- google analytics for #{@account_id} -->" |
| + | end |
| + | end |
| + | |
| + | ::Liquid::Template.register_tag('google_analytics', GoogleAnalytics) |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/tags/inline_editor.rb b/lib/locomotive/builder/liquid/tags/inline_editor.rb
+16
-0
| @@ | @@ -0,0 +1,16 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Tags |
| + | class InlineEditor < ::Liquid::Tag |
| + | |
| + | def render(context) |
| + | '' |
| + | end |
| + | end |
| + | |
| + | ::Liquid::Template.register_tag('inline_editor', InlineEditor) |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/tags/locale_switcher.rb b/lib/locomotive/builder/liquid/tags/locale_switcher.rb
+72
-0
| @@ | @@ -0,0 +1,72 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Tags |
| + | |
| + | # Display the links to change the locale of the current page |
| + | # |
| + | # Usage: |
| + | # |
| + | # {% locale_switcher %} => <div id="locale-switcher"><a href="/features" class="current en">Features</a><a href="/fr/fonctionnalites" class="fr">Fonctionnalités</a></div> |
| + | # |
| + | # {% locale_switcher label: locale, sep: ' - ' } |
| + | # |
| + | # options: |
| + | # - label: iso (de, fr, en, ...etc), locale (Deutsch, Français, English, ...etc), title (page title) |
| + | # - sep: piece of html code separating 2 locales |
| + | # |
| + | # notes: |
| + | # - "iso" is the default choice for label |
| + | # - " | " is the default separating code |
| + | # |
| + | class LocaleSwitcher < ::Liquid::Tag |
| + | |
| + | Syntax = /(#{::Liquid::Expression}+)?/ |
| + | |
| + | def initialize(tag_name, markup, tokens, context) |
| + | @options = { :label => 'iso', :sep => ' | ' } |
| + | |
| + | if markup =~ Syntax |
| + | markup.scan(::Liquid::TagAttributes) { |key, value| @options[key.to_sym] = value.gsub(/"|'/, '') } |
| + | |
| + | @options[:exclude] = Regexp.new(@options[:exclude]) if @options[:exclude] |
| + | else |
| + | raise ::Liquid::SyntaxError.new("Syntax Error in 'locale_switcher' - Valid syntax: locale_switcher <options>") |
| + | end |
| + | |
| + | super |
| + | end |
| + | |
| + | def render(context) |
| + | @site, @page = context.registers[:site], context.registers[:page] |
| + | |
| + | output = %(<div id="locale-switcher">) |
| + | |
| + | output += @site.locales.collect do |locale| |
| + | fullpath = locale == context['current_locale'] ? '/' : locale |
| + | |
| + | %(<a href="/#{fullpath}" class="#{locale} #{'current' if locale == context['current_locale']}">#{link_label(locale)}</a>) |
| + | end.join(@options[:sep]) |
| + | |
| + | output += %(</div>) |
| + | end |
| + | |
| + | private |
| + | |
| + | def link_label(locale) |
| + | case @options[:label] |
| + | when :iso then locale |
| + | when :locale then I18n.t("locomotive.locales.#{locale}", :locale => locale) |
| + | when :title then @page.title # FIXME: this returns nil if the page has not been translated in the locale |
| + | else |
| + | locale |
| + | end |
| + | end |
| + | |
| + | end |
| + | |
| + | ::Liquid::Template.register_tag('locale_switcher', LocaleSwitcher) |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/tags/nav.rb b/lib/locomotive/builder/liquid/tags/nav.rb
+167
-0
| @@ | @@ -0,0 +1,167 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Tags |
| + | # Display the children pages of the site, current page or the parent page. If not precised, nav is applied on the current page. |
| + | # The html output is based on the ul/li tags. |
| + | # |
| + | # Usage: |
| + | # |
| + | # {% nav site %} => <ul class="nav"><li class="on"><a href="/features">Features</a></li></ul> |
| + | # |
| + | # {% nav site, no_wrapper: true, exclude: 'contact|about', id: 'main-nav', class: 'nav', active_class: 'on' } |
| + | # |
| + | class Nav < ::Liquid::Tag |
| + | |
| + | Syntax = /(#{::Liquid::Expression}+)?/ |
| + | |
| + | attr_accessor :current_page, :mounting_point |
| + | |
| + | def initialize(tag_name, markup, tokens, context) |
| + | if markup =~ Syntax |
| + | @source = ($1 || 'page').gsub(/"|'/, '') |
| + | @options = { :id => 'nav', :class => '', :active_class => 'on', :bootstrap => false } |
| + | markup.scan(::Liquid::TagAttributes) { |key, value| @options[key.to_sym] = value.gsub(/"|'/, '') } |
| + | |
| + | @options[:exclude] = Regexp.new(@options[:exclude]) if @options[:exclude] |
| + | |
| + | if @options[:snippet] |
| + | if template = self.parse_snippet_template(context, @options[:snippet]) |
| + | @options[:liquid_render] = template |
| + | end |
| + | end |
| + | else |
| + | raise ::Liquid::SyntaxError.new("Syntax Error in 'nav' - Valid syntax: nav <site|parent|page|<path to a page>> <options>") |
| + | end |
| + | |
| + | super |
| + | end |
| + | |
| + | def render(context) |
| + | self.set_accessors_from_context(context) |
| + | |
| + | children_output = [] |
| + | |
| + | entries = self.fetch_entries |
| + | |
| + | entries.each_with_index do |p, index| |
| + | css = [] |
| + | css << 'first' if index == 0 |
| + | css << 'last' if index == entries.size - 1 |
| + | |
| + | children_output << render_entry_link(p, css.join(' '), 1) |
| + | end |
| + | |
| + | output = children_output.join("\n") |
| + | |
| + | if @options[:no_wrapper] != 'true' |
| + | output = %{<ul id="#{@options[:id]}" class="#{@options[:class]}">\n#{output}</ul>} |
| + | end |
| + | |
| + | output |
| + | end |
| + | |
| + | protected |
| + | |
| + | def set_accessors_from_context(context) |
| + | self.current_page = context.registers[:page] |
| + | self.mounting_point = context.registers[:mounting_point] |
| + | end |
| + | |
| + | def parse_snippet_template(context, template_name) |
| + | source = if template_name.include?('{') |
| + | template_name |
| + | else |
| + | context[:mounting_point].snippets[template_name].try(:source) |
| + | end |
| + | |
| + | source ? ::Liquid::Template.parse(source) : nil |
| + | end |
| + | |
| + | def fetch_entries |
| + | children = (case @source |
| + | when 'site' then self.mounting_point.pages['index'] |
| + | when 'parent' then self.current_page.parent || self.current_page |
| + | when 'page' then self.current_page |
| + | else |
| + | self.mounting_point.pages[@source] |
| + | end).children.clone |
| + | |
| + | children.delete_if { |p| !include_page?(p) } |
| + | end |
| + | |
| + | # Determines whether or not a page should be a part of the menu |
| + | def include_page?(page) |
| + | if !page.listed? || page.templatized? || !page.published? |
| + | false |
| + | elsif @options[:exclude] |
| + | (page.fullpath =~ @options[:exclude]).nil? |
| + | else |
| + | true |
| + | end |
| + | end |
| + | |
| + | # Returns a list element, a link to the page and its children |
| + | def render_entry_link(page, css, depth) |
| + | selected = self.current_page.fullpath =~ /^#{page.fullpath}/ ? " #{@options[:active_class]}" : '' |
| + | |
| + | icon = @options[:icon] ? '<span></span>' : '' |
| + | |
| + | title = @options[:liquid_render] ? @options[:liquid_render].render('page' => page) : page.title |
| + | |
| + | label = %{#{icon if @options[:icon] != 'after' }#{title}#{icon if @options[:icon] == 'after' }} |
| + | |
| + | dropdow = "" |
| + | link_options = "" |
| + | href = ::I18n.locale.to_s == self.mounting_point.default_locale.to_s ? "/#{page.fullpath}" : "/#{::I18n.locale}/#{page.fullpath}" |
| + | caret = "" |
| + | |
| + | if render_children_for_page?(page, depth) && bootstrap? |
| + | dropdow = "dropdown" |
| + | link_options = %{class="dropdown-toggle" data-toggle="dropdown"} |
| + | href = "#" |
| + | caret = %{<b class="caret"></b>} |
| + | end |
| + | |
| + | output = %{<li id="#{page.slug.to_s.dasherize}-link" class="link#{selected} #{css} #{dropdow}">} |
| + | output << %{<a href="#{href}" #{link_options}>#{label} #{caret}</a>} |
| + | output << render_entry_children(page, depth.succ) if (depth.succ <= @options[:depth].to_i) |
| + | output << %{</li>} |
| + | |
| + | output.strip |
| + | end |
| + | |
| + | def render_children_for_page?(page, depth) |
| + | depth.succ <= @options[:depth].to_i && page.children.reject { |c| !include_page?(c) }.any? |
| + | end |
| + | |
| + | # Recursively creates a nested unordered list for the depth specified |
| + | def render_entry_children(page, depth) |
| + | output = %{} |
| + | |
| + | children = page.children.reject { |c| !include_page?(c) } |
| + | if children.present? |
| + | output = %{<ul id="#{@options[:id]}-#{page.slug.to_s.dasherize}" class="#{bootstrap? ? "dropdown-menu" : ""}">} |
| + | children.each do |c, page| |
| + | css = [] |
| + | css << 'first' if children.first == c |
| + | css << 'last' if children.last == c |
| + | |
| + | output << render_entry_link(c, css.join(' '),depth) |
| + | end |
| + | output << %{</ul>} |
| + | end |
| + | |
| + | output |
| + | end |
| + | |
| + | def bootstrap? |
| + | @options[:bootstrap] == 'true' || @options[:bootstrap] == true |
| + | end |
| + | |
| + | ::Liquid::Template.register_tag('nav', Nav) |
| + | end |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/tags/paginate.rb b/lib/locomotive/builder/liquid/tags/paginate.rb
+105
-0
| @@ | @@ -0,0 +1,105 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Tags |
| + | |
| + | # Paginate a collection |
| + | # |
| + | # Usage: |
| + | # |
| + | # {% paginate contents.projects by 5 %} |
| + | # {% for project in paginate.collection %} |
| + | # {{ project.name }} |
| + | # {% endfor %} |
| + | # {% endpaginate %} |
| + | # |
| + | |
| + | class Paginate < ::Liquid::Block |
| + | |
| + | Syntax = /(#{::Liquid::Expression}+)\s+by\s+([0-9]+)/ |
| + | |
| + | def initialize(tag_name, markup, tokens, context) |
| + | if markup =~ Syntax |
| + | @collection_name = $1 |
| + | @per_page = $2.to_i |
| + | else |
| + | raise ::Liquid::SyntaxError.new("Syntax Error in 'paginate' - Valid syntax: paginate <collection> by <number>") |
| + | end |
| + | |
| + | super |
| + | end |
| + | |
| + | def render(context) |
| + | context.stack do |
| + | collection = context[@collection_name] |
| + | |
| + | raise ::Liquid::ArgumentError.new("Cannot paginate array '#{@collection_name}'. Not found.") if collection.nil? |
| + | |
| + | pagination = collection.send(:paginate, { |
| + | :page => context['current_page'], |
| + | :per_page => @per_page }).stringify_keys! |
| + | |
| + | page_count, current_page = pagination['total_pages'], pagination['current_page'] |
| + | |
| + | path = sanitize_path(context['fullpath']) |
| + | |
| + | pagination['previous'] = link(I18n.t('pagination.previous'), current_page - 1, path) if pagination['previous_page'] |
| + | pagination['next'] = link(I18n.t('pagination.next'), current_page + 1, path) if pagination['next_page'] |
| + | pagination['parts'] = [] |
| + | |
| + | hellip_break = false |
| + | |
| + | if page_count > 1 |
| + | 1.upto(page_count) do |page| |
| + | if current_page == page |
| + | pagination['parts'] << no_link(page) |
| + | elsif page == 1 |
| + | pagination['parts'] << link(page, page, path) |
| + | elsif page == page_count - 1 |
| + | pagination['parts'] << link(page, page, path) |
| + | elsif page <= current_page - window_size or page >= current_page + window_size |
| + | next if hellip_break |
| + | pagination['parts'] << no_link('…') |
| + | hellip_break = true |
| + | next |
| + | else |
| + | pagination['parts'] << link(page, page, path) |
| + | end |
| + | |
| + | hellip_break = false |
| + | end |
| + | end |
| + | |
| + | context['paginate'] = pagination |
| + | |
| + | render_all(@nodelist, context) |
| + | end |
| + | end |
| + | |
| + | private |
| + | |
| + | def sanitize_path(path) |
| + | _path = path.gsub(/page=[0-9]+&?/, '').gsub(/_pjax=true&?/, '') |
| + | _path = _path.slice(0..-2) if _path.last == '?' || _path.last == '&' |
| + | _path |
| + | end |
| + | |
| + | def window_size |
| + | 3 |
| + | end |
| + | |
| + | def no_link(title) |
| + | { 'title' => title, 'is_link' => false, 'hellip_break' => title == '…' } |
| + | end |
| + | |
| + | def link(title, page, path) |
| + | _path = %(#{path}#{path.include?('?') ? '&' : '?'}page=#{page}) |
| + | { 'title' => title, 'url' => _path, 'is_link' => true } |
| + | end |
| + | end |
| + | |
| + | ::Liquid::Template.register_tag('paginate', Paginate) |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/tags/seo.rb b/lib/locomotive/builder/liquid/tags/seo.rb
+74
-0
| @@ | @@ -0,0 +1,74 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Tags |
| + | module SEO |
| + | |
| + | class Base < ::Liquid::Tag |
| + | |
| + | def render(context) |
| + | %{ |
| + | #{self.render_title(context)} |
| + | #{self.render_metadata(context)} |
| + | } |
| + | end |
| + | |
| + | protected |
| + | |
| + | def render_title(context) |
| + | title = self.value_for(:seo_title, context) |
| + | title = context.registers[:site].name if title.blank? |
| + | |
| + | %{ |
| + | <title>#{title}</title> |
| + | } |
| + | end |
| + | |
| + | def render_metadata(context) |
| + | %{ |
| + | <meta name="description" content="#{self.value_for(:meta_description, context)}" /> |
| + | <meta name="keywords" content="#{self.value_for(:meta_keywords, context)}" /> |
| + | } |
| + | end |
| + | |
| + | # Removes whitespace and quote characters from the input |
| + | def sanitized_string(string) |
| + | string ? string.strip.gsub(/"/, '') : '' |
| + | end |
| + | |
| + | def value_for(attribute, context) |
| + | object = self.metadata_object(context) |
| + | value = object.try(attribute.to_sym).blank? ? context.registers[:site].send(attribute.to_sym) : object.send(attribute.to_sym) |
| + | self.sanitized_string(value) |
| + | end |
| + | |
| + | def metadata_object(context) |
| + | context['content_instance'] || context['page'] |
| + | end |
| + | end |
| + | |
| + | class Title < Base |
| + | |
| + | def render(context) |
| + | self.render_title(context) |
| + | end |
| + | |
| + | end |
| + | |
| + | class Metadata < Base |
| + | |
| + | def render(context) |
| + | self.render_metadata(context) |
| + | end |
| + | |
| + | end |
| + | |
| + | end |
| + | |
| + | ::Liquid::Template.register_tag('seo', SEO::Base) |
| + | ::Liquid::Template.register_tag('seo_title', SEO::Title) |
| + | ::Liquid::Template.register_tag('seo_metadata', SEO::Metadata) |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/tags/snippet.rb b/lib/locomotive/builder/liquid/tags/snippet.rb
+40
-0
| @@ | @@ -0,0 +1,40 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Tags |
| + | |
| + | class Snippet < ::Liquid::Include |
| + | |
| + | def render(context) |
| + | source = context.registers[:mounting_point].snippets[@template_name].try(:source) |
| + | |
| + | partial = ::Liquid::Template.parse(source) |
| + | |
| + | variable = context[@variable_name || @template_name[1..-2]] |
| + | |
| + | context.stack do |
| + | @attributes.each do |key, value| |
| + | context[key] = context[value] |
| + | end |
| + | |
| + | output = (if variable.is_a?(Array) |
| + | variable.collect do |variable| |
| + | context[@template_name[1..-2]] = variable |
| + | partial.render(context) |
| + | end |
| + | else |
| + | context[@template_name[1..-2]] = variable |
| + | partial.render(context) |
| + | end) |
| + | |
| + | output |
| + | end |
| + | end |
| + | |
| + | end |
| + | |
| + | ::Liquid::Template.register_tag('include', Snippet) |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/liquid/tags/with_scope.rb b/lib/locomotive/builder/liquid/tags/with_scope.rb
+43
-0
| @@ | @@ -0,0 +1,43 @@ |
| + | module Locomotive |
| + | module Builder |
| + | module Liquid |
| + | module Tags |
| + | class WithScope < ::Liquid::Block |
| + | |
| + | def initialize(tag_name, markup, tokens, context) |
| + | @options = {} |
| + | |
| + | markup.scan(::Liquid::TagAttributes) do |key, value| |
| + | @options[key] = value |
| + | end |
| + | |
| + | super |
| + | end |
| + | |
| + | def render(context) |
| + | context.stack do |
| + | context['with_scope'] = decode(@options, context) |
| + | render_all(@nodelist, context) |
| + | end |
| + | end |
| + | |
| + | private |
| + | |
| + | def decode(attributes, context) |
| + | attributes.each_pair do |key, value| |
| + | attributes[key] = (case value |
| + | when /^true|false$/i then value == 'true' |
| + | when /^[0-9]+$/ then value.to_i |
| + | when /^["|'](.+)["|']$/ then $1.gsub(/^["|']/, '').gsub(/["|']$/, '') |
| + | else |
| + | context[value] || value |
| + | end) |
| + | end |
| + | end |
| + | end |
| + | |
| + | ::Liquid::Template.register_tag('with_scope', WithScope) |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/misc.rb b/lib/locomotive/builder/misc.rb
+1
-0
| @@ | @@ -0,0 +1 @@ |
| + | require 'locomotive/builder/misc/httparty.rb' |
| \ No newline at end of file | |
locomotive/builder/misc/httparty.rb b/lib/locomotive/builder/misc/httparty.rb
+46
-0
| @@ | @@ -0,0 +1,46 @@ |
| + | require 'uri' |
| + | |
| + | module Locomotive |
| + | module Builder |
| + | module Httparty |
| + | class Webservice |
| + | |
| + | include ::HTTParty |
| + | |
| + | def self.consume(url, options = {}) |
| + | url = ::HTTParty.normalize_base_uri(url) |
| + | |
| + | uri = URI.parse(url) |
| + | options[:base_uri] = "#{uri.scheme}://#{uri.host}" |
| + | options[:base_uri] += ":#{uri.port}" if uri.port != 80 |
| + | path = uri.request_uri |
| + | |
| + | options.delete(:format) if options[:format] == 'default' |
| + | |
| + | username, password = options.delete(:username), options.delete(:password) |
| + | options[:basic_auth] = { username: username, password: password } if username |
| + | |
| + | path ||= '/' |
| + | |
| + | # puts "[WebService] consuming #{path}, #{options.inspect}" |
| + | |
| + | response = self.get(path, options) |
| + | |
| + | if response.code == 200 |
| + | if response.respond_to?(:underscore_keys) |
| + | response.underscore_keys |
| + | else |
| + | response.collect(&:underscore_keys) |
| + | end |
| + | else |
| + | # TODO: handle errors |
| + | puts "[Locomotive][Builder][Error] consuming #{path}, #{options.inspect}, response = #{response.inspect}" |
| + | nil |
| + | end |
| + | |
| + | end |
| + | |
| + | end |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/server.rb b/lib/locomotive/builder/server.rb
+33
-11
| @@ | @@ -1,23 +1,45 @@ |
| - | require "locomotive/builder/server/middleware" |
| - | require "locomotive/builder/server/index" |
| - | require "locomotive/builder/server/pages" |
| - | require "locomotive/builder/server/not_found" |
| + | require 'rack/showexceptions' |
| + | require 'locomotive/builder/server/middleware' |
| + | require 'locomotive/builder/server/favicon' |
| + | require 'locomotive/builder/server/dynamic_assets' |
| + | require 'locomotive/builder/server/path' |
| + | require 'locomotive/builder/server/locale' |
| + | require 'locomotive/builder/server/page' |
| + | require 'locomotive/builder/server/not_found' |
| + | require 'locomotive/builder/server/renderer' |
| + | |
| + | require 'locomotive/builder/liquid' |
| + | require 'locomotive/builder/misc' |
| module Locomotive::Builder | |
| class Server | |
| + | |
| def initialize(reader) | |
| @reader = reader | |
| @app = Rack::Builder.new do | |
| - | use Rack::Lint |
| - | use Index |
| - | use Pages |
| - | run NotFound.new |
| - | end |
| + | use Rack::ShowExceptions |
| + | use Rack::Lint |
| + | |
| + | use Rack::Static, { |
| + | urls: ['/images', '/fonts', '/samples'], |
| + | root: File.join(reader.mounting_point.path, 'public') |
| + | } |
| + | use Favicon |
| + | use DynamicAssets |
| + | use Path |
| + | use Locale |
| + | use Page |
| + | use NotFound |
| + | use Renderer |
| + | |
| + | run Renderer.new |
| + | end |
| end | |
| - | |
| + | |
| def call(env) | |
| - | env["steam.mounting_point"] = @reader.mounting_point |
| + | env['builder.mounting_point'] = @reader.mounting_point |
| @app.call(env) | |
| end | |
| + | |
| end | |
| end | |
| \ No newline at end of file | |
locomotive/builder/server/dynamic_assets.rb b/lib/locomotive/builder/server/dynamic_assets.rb
+31
-0
| @@ | @@ -0,0 +1,31 @@ |
| + | module Locomotive::Builder |
| + | class Server |
| + | |
| + | class DynamicAssets < Middleware |
| + | |
| + | def call(env) |
| + | self.set_accessors(env) |
| + | |
| + | path = env['PATH_INFO'] |
| + | |
| + | if path =~ /^\/(stylesheets|javascripts)\// |
| + | |
| + | mime_type = MIME::Types.type_for(path).first.try(:to_s) || 'text/plain' |
| + | asset = self.mounting_point.theme_assets.detect do |_asset| |
| + | _asset.path == path |
| + | end |
| + | |
| + | if asset |
| + | [200, { 'Content-Type' => mime_type }, [asset.content]] |
| + | else |
| + | [404, { 'Content-Type' => mime_type }, ['Asset not found']] |
| + | end |
| + | else |
| + | app.call(env) |
| + | end |
| + | end |
| + | |
| + | end |
| + | |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/server/favicon.rb b/lib/locomotive/builder/server/favicon.rb
+17
-0
| @@ | @@ -0,0 +1,17 @@ |
| + | module Locomotive::Builder |
| + | class Server |
| + | |
| + | class Favicon < Middleware |
| + | |
| + | def call(env) |
| + | if env['PATH_INFO'] == '/favicon.ico' |
| + | [200, { 'Content-Type' => 'image/vnd.microsoft.icon' }, ['']] |
| + | else |
| + | app.call(env) |
| + | end |
| + | end |
| + | |
| + | end |
| + | |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/server/index.rb b/lib/locomotive/builder/server/index.rb
+0
-13
| @@ | @@ -1,13 +0,0 @@ |
| - | module Locomotive::Builder |
| - | class Server |
| - | class Index < Middleware |
| - | def call(env) |
| - | if env['PATH_INFO'] == '/' |
| - | [404, {'Content-Type' => 'text/html'}, [env["steam.mounting_point"].pages["index"].source]] |
| - | else |
| - | super |
| - | end |
| - | end |
| - | end |
| - | end |
| - | end |
| \ No newline at end of file | |
locomotive/builder/server/locale.rb b/lib/locomotive/builder/server/locale.rb
+43
-0
| @@ | @@ -0,0 +1,43 @@ |
| + | module Locomotive::Builder |
| + | class Server |
| + | |
| + | # Set the locale from the path if possible or use the default one |
| + | # Examples: |
| + | # /fr/index => locale = :fr |
| + | # /fr/ => locale = :fr |
| + | # /index => locale = :en (default one) |
| + | # |
| + | class Locale < Middleware |
| + | |
| + | def call(env) |
| + | self.set_accessors(env) |
| + | |
| + | self.set_locale!(env) |
| + | |
| + | app.call(env) |
| + | end |
| + | |
| + | protected |
| + | |
| + | def set_locale!(env) |
| + | path = env['builder.path'] |
| + | locale = self.mounting_point.default_locale |
| + | |
| + | if path =~ /^(#{self.mounting_point.locales.join('|')})+(\/|$)/ |
| + | locale = $1 |
| + | path = path.gsub($1 + $2, '') |
| + | |
| + | # TODO: I18n.locale ??? |
| + | |
| + | Locomotive::Mounter.locale = locale |
| + | end |
| + | |
| + | puts "[Builder|Locale] path = #{path.inspect}, locale = #{locale.inspect}" |
| + | |
| + | env['builder.locale'] = locale |
| + | env['builder.path'] = path |
| + | end |
| + | |
| + | end |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/server/middleware.rb b/lib/locomotive/builder/server/middleware.rb
+26
-4
| @@ | @@ -1,15 +1,37 @@ |
| module Locomotive::Builder | |
| class Server | |
| + | |
| class Middleware | |
| - | attr_accessor :app |
| - | |
| - | def initialize(app) |
| + | |
| + | attr_accessor :app, :request |
| + | |
| + | attr_accessor :mounting_point, :page |
| + | |
| + | def initialize(app = nil) |
| @app = app | |
| end | |
| - | |
| + | |
| def call(env) | |
| app.call(env) | |
| end | |
| + | |
| + | protected |
| + | |
| + | def set_accessors(env) |
| + | self.request = Rack::Request.new(env) |
| + | self.mounting_point = env['builder.mounting_point'] |
| + | self.page = env['builder.page'] |
| + | end |
| + | |
| + | def site |
| + | self.mounting_point.site |
| + | end |
| + | |
| + | def params |
| + | self.request.params |
| + | end |
| + | |
| end | |
| + | |
| end | |
| end | |
| \ No newline at end of file | |
locomotive/builder/server/not_found.rb b/lib/locomotive/builder/server/not_found.rb
+11
-2
| @@ | @@ -1,9 +1,18 @@ |
| module Locomotive::Builder | |
| class Server | |
| - | class NotFound |
| + | |
| + | class NotFound < Middleware |
| + | |
| def call(env) | |
| - | [404, {'Content-Type' => 'text/html'}, [env["steam.mounting_point"].pages["404"].source]] |
| + | self.set_accessors(env) |
| + | |
| + | if self.page.nil? |
| + | env['builder.page'] = self.mounting_point.pages['404'] |
| + | end |
| + | |
| + | app.call(env) |
| end | |
| + | |
| end | |
| end | |
| end | |
| \ No newline at end of file | |
locomotive/builder/server/page.rb b/lib/locomotive/builder/server/page.rb
+61
-0
| @@ | @@ -0,0 +1,61 @@ |
| + | module Locomotive::Builder |
| + | class Server |
| + | |
| + | # Sanitize the path from the previous middleware in order |
| + | # to make it work for the renderer. |
| + | # |
| + | class Page < Middleware |
| + | |
| + | def call(env) |
| + | self.set_accessors(env) |
| + | |
| + | self.set_page!(env) |
| + | |
| + | app.call(env) |
| + | end |
| + | |
| + | protected |
| + | |
| + | def set_page!(env) |
| + | path = env['builder.path'] |
| + | |
| + | page = self.fetch_page(path) |
| + | |
| + | puts "[Builder|Page] #{page.inspect}" |
| + | |
| + | env['builder.page'] = page |
| + | end |
| + | |
| + | def fetch_page(path) |
| + | matchers = self.path_combinations(path) |
| + | |
| + | self.mounting_point.pages.values.detect do |_page| |
| + | matchers.include?(_page.safe_fullpath) || |
| + | matchers.include?(_page.safe_fullpath.underscore) |
| + | end |
| + | end |
| + | |
| + | def path_combinations(path) |
| + | self._path_combinations(path.split('/')) |
| + | end |
| + | |
| + | def _path_combinations(segments, can_include_template = true) |
| + | return nil if segments.empty? |
| + | |
| + | segment = segments.shift |
| + | |
| + | (can_include_template ? [segment, '*'] : [segment]).map do |_segment| |
| + | if (_combinations = _path_combinations(segments.clone, can_include_template && _segment != '*')) |
| + | [*_combinations].map do |_combination| |
| + | File.join(_segment, _combination) |
| + | end |
| + | else |
| + | [_segment] |
| + | end |
| + | end.flatten |
| + | end |
| + | |
| + | end |
| + | |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/server/pages.rb b/lib/locomotive/builder/server/pages.rb
+0
-14
| @@ | @@ -1,14 +0,0 @@ |
| - | module Locomotive::Builder |
| - | class Server |
| - | class Pages < Middleware |
| - | def call(env) |
| - | requested = env['PATH_INFO'].gsub(/^\//, '') |
| - | if env["steam.mounting_point"].pages.has_key?(requested) |
| - | [200, {'Content-Type' => 'text/html'}, [env["steam.mounting_point"].pages[requested].source]] |
| - | else |
| - | super |
| - | end |
| - | end |
| - | end |
| - | end |
| - | end |
| \ No newline at end of file | |
locomotive/builder/server/path.rb b/lib/locomotive/builder/server/path.rb
+34
-0
| @@ | @@ -0,0 +1,34 @@ |
| + | module Locomotive::Builder |
| + | class Server |
| + | |
| + | # Sanitize the path from the previous middleware in order |
| + | # to make it work for the renderer. |
| + | # |
| + | class Path < Middleware |
| + | |
| + | def call(env) |
| + | self.set_accessors(env) |
| + | |
| + | self.set_path!(env) |
| + | |
| + | app.call(env) |
| + | end |
| + | |
| + | protected |
| + | |
| + | def set_path!(env) |
| + | path = env['PATH_INFO'].clone |
| + | |
| + | path.gsub!(/\.[a-zA-Z][a-zA-Z0-9]{2,}$/, '') |
| + | path.gsub!(/^\//, '') |
| + | path.gsub!(/^[A-Z]:\//, '') |
| + | |
| + | path = 'index' if path.blank? |
| + | |
| + | env['builder.path'] = path |
| + | end |
| + | |
| + | end |
| + | |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotive/builder/server/renderer.rb b/lib/locomotive/builder/server/renderer.rb
+108
-0
| @@ | @@ -0,0 +1,108 @@ |
| + | module Locomotive::Builder |
| + | class Server |
| + | |
| + | class Renderer < Middleware |
| + | |
| + | def call(env) |
| + | self.set_accessors(env) |
| + | |
| + | puts "[Builder|Renderer] page = #{page.inspect}" |
| + | |
| + | if self.page |
| + | if self.page.redirect? |
| + | [self.page.redirect_type, { 'Location' => self.page.redirect_url, 'Content-Type' => 'text/html' }, []] |
| + | else |
| + | type = self.page.response_type || 'text/html' |
| + | html = self.render |
| + | |
| + | [200, { 'Content-Type' => type }, [html]] |
| + | end |
| + | else |
| + | puts "argggg" |
| + | # no page at all, even not the 404 page |
| + | [404, { 'Content-Type' => 'text/html' }, ['Page not found']] |
| + | end |
| + | end |
| + | |
| + | protected |
| + | |
| + | def render |
| + | context = self.locomotive_context |
| + | |
| + | template = ::Liquid::Template.parse(self.page.source, { |
| + | page: self.page, |
| + | mounting_point: self.mounting_point |
| + | }) |
| + | |
| + | template.render(context) |
| + | end |
| + | |
| + | # Build the Liquid context used to render the Locomotive page. It |
| + | # stores both assigns and registers. |
| + | # |
| + | # @param [ Hash ] other_assigns Assigns coming for instance from the controler (optional) |
| + | # |
| + | # @return [ Object ] A new instance of the Liquid::Context class. |
| + | # |
| + | def locomotive_context(other_assigns = {}) |
| + | assigns = self.locomotive_default_assigns |
| + | |
| + | # process data from the session |
| + | # assigns.merge!(self.locomotive_flash_assigns) |
| + | |
| + | assigns.merge!(other_assigns) |
| + | |
| + | # TODO: templatized page |
| + | |
| + | # if defined?(self.page) && self.page.templatized? # add instance from content type |
| + | # content_entry = self.page.content_entry.to_liquid |
| + | # ['content_entry', 'entry', @page.target_entry_name].each do |key| |
| + | # assigns[key] = content_entry |
| + | # end |
| + | # end |
| + | |
| + | # Tip: switch from false to true to enable the re-thrown exception flag |
| + | ::Liquid::Context.new({}, assigns, self.locomotive_default_registers, true) |
| + | end |
| + | |
| + | # Return the default Liquid assigns used inside the Locomotive Liquid context |
| + | # |
| + | # @return [ Hash ] The default liquid assigns object |
| + | # |
| + | def locomotive_default_assigns |
| + | { |
| + | 'site' => self.site.to_liquid, |
| + | 'page' => self.page, |
| + | 'models' => Locomotive::Builder::Liquid::Drops::ContentTypes.new, |
| + | 'contents' => Locomotive::Builder::Liquid::Drops::ContentTypes.new, |
| + | 'current_page' => self.params[:page], |
| + | 'params' => self.params, |
| + | 'path' => self.request.path, |
| + | 'fullpath' => self.request.fullpath, |
| + | 'url' => self.request.url, |
| + | 'now' => Time.now.utc, |
| + | 'today' => Date.today, |
| + | 'locale' => I18n.locale.to_s, |
| + | 'default_locale' => self.mounting_point.default_locale.to_s, |
| + | 'locales' => self.mounting_point.locales.map(&:to_s), |
| + | 'current_user' => {} |
| + | } |
| + | end |
| + | |
| + | # Return the default Liquid registers used inside the Locomotive Liquid context |
| + | # |
| + | # @return [ Hash ] The default liquid registers object |
| + | # |
| + | def locomotive_default_registers |
| + | { |
| + | site: self.site, |
| + | page: self.page, |
| + | mounting_point: self.mounting_point, |
| + | inline_editor: false |
| + | } |
| + | end |
| + | |
| + | end |
| + | |
| + | end |
| + | end |
| \ No newline at end of file | |
locomotivecms_builder.gemspec
+23
-14
| @@ | @@ -4,25 +4,34 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) |
| require 'locomotive/builder/version' | |
| Gem::Specification.new do |gem| | |
| - | gem.name = "locomotivecms_builder" |
| + | gem.name = 'locomotivecms_builder' |
| gem.version = Locomotive::Builder::VERSION | |
| - | gem.authors = ["Rodrigo Alvarez"] |
| - | gem.email = ["papipo@gmail.com"] |
| + | gem.authors = ['Didier Lafforgue', 'Rodrigo Alvarez'] |
| + | gem.email = ['papipo@gmail.com'] |
| gem.description = %q{TODO: Write a gem description} | |
| gem.summary = %q{TODO: Write a gem summary} | |
| - | gem.homepage = "" |
| + | gem.homepage = '' |
| gem.files = `git ls-files`.split($/) | |
| gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } | |
| gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) | |
| - | gem.require_paths = ["lib"] |
| - | gem.executables = ["builder"] |
| - | |
| - | gem.add_dependency "thor" |
| - | gem.add_dependency "thin" |
| - | # gem.add_dependency "locomotivecms_mounter" # remove from Gemfile before adding it here |
| - | gem.add_development_dependency "rspec" |
| - | gem.add_development_dependency "vcr" |
| - | gem.add_development_dependency "webmock", "~> 1.8.0" |
| - | gem.add_development_dependency "rack-test" |
| + | gem.require_paths = ['lib'] |
| + | gem.executables = ['builder'] |
| + | |
| + | gem.add_dependency 'thor' |
| + | gem.add_dependency 'thin' |
| + | gem.add_dependency 'locomotive_liquid', '~> 2.4.1' |
| + | gem.add_dependency 'RedCloth', '~> 4.2.9' |
| + | gem.add_dependency 'dragonfly', '~> 0.9.12' |
| + | gem.add_dependency 'rack-cache', '~> 1.1' |
| + | gem.add_dependency 'rack-rescue', '~> 0.1.2' |
| + | gem.add_dependency 'rmagick', '2.12.2' |
| + | gem.add_dependency 'httmultiparty', '~> 0.3.8' |
| + | gem.add_dependency 'will_paginate', '~> 3.0.3' |
| + | # gem.add_dependency 'locomotivecms_mounter' # remove from Gemfile before adding it here |
| + | |
| + | gem.add_development_dependency 'rspec' |
| + | gem.add_development_dependency 'vcr' |
| + | gem.add_development_dependency 'webmock', '~> 1.8.0' |
| + | gem.add_development_dependency 'rack-test' |
| end | |