refactor the nav liquid tag + close #84

did committed Jul 02, 2013
commit 55e320ca96964361643898cb44fb621cba1d7f26
Showing 7 changed files with 269 additions and 82 deletions
locomotive/wagon/liquid/scopeable.rb b/lib/locomotive/wagon/liquid/scopeable.rb +32 -0
@@ @@ -0,0 +1,32 @@
+ module Locomotive
+ module Wagon
+ module Liquid
+ module Scopeable
+
+ def apply_scope(entries)
+ if @context['with_scope'].blank?
+ entries
+ else
+ collection = []
+
+ conditions = @context['with_scope'].clone.delete_if { |k, _| %w(order_by per_page page).include?(k) }
+
+ entries.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
+ collection
+ end
+ end
+
+ end
+ end
+ end
+ end
\ No newline at end of file
locomotive/wagon/liquid/tags/nav.rb b/lib/locomotive/wagon/liquid/tags/nav.rb +192 -73
@@ @@ -2,6 +2,7 @@ module Locomotive
module Wagon
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.
#
@@ @@ -20,16 +21,8 @@ module Locomotive
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
+ self.set_options(markup, context)
else
raise ::Liquid::SyntaxError.new("Syntax Error in 'nav' - Valid syntax: nav <site|parent|page|<path to a page>> <options>")
end
@@ @@ -40,45 +33,44 @@ module Locomotive
def render(context)
self.set_accessors_from_context(context)
- children_output = []
-
entries = self.fetch_entries
+ output = self.build_entries_output(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'
- list_class = !@options[:class].blank? ? %( class="#{@options[:class]}") : ''
- output = %{<nav id="#{@options[:id]}"#{list_class}><ul>\n#{output}</ul></nav>}
+ if self.no_wrapper?
+ output
+ else
+ self.render_tag(:nav, id: @options[:id], css: @options[:class]) do
+ self.render_tag(:ul) { output }
+ end
end
-
- output
end
protected
- def set_accessors_from_context(context)
- self.current_page = context.registers[:page]
- self.mounting_point = context.registers[:mounting_point]
- end
+ # Build recursively the links of all the pages.
+ #
+ # @param [ Array ] entries List of pages
+ #
+ # @return [ String ] The final HTML output
+ #
+ def build_entries_output(entries, depth = 1)
+ output = []
- def parse_snippet_template(context, template_name)
- source = if template_name.include?('{')
- template_name
- else
- context[:mounting_point].snippets[template_name].try(:source)
+ entries.each_with_index do |page, index|
+ css = []
+ css << 'first' if index == 0
+ css << 'last' if index == entries.size - 1
+
+ output << self.render_entry_link(page, css.join(' '), depth)
end
- source ? ::Liquid::Template.parse(source) : nil
+ output.join("\n")
end
+ # Get all the children of a source: site (index page), parent or page.
+ #
+ # @return [ Array ] List of pages
+ #
def fetch_entries
children = (case @source
when 'site' then self.mounting_point.pages['index']
@@ @@ -91,7 +83,12 @@ module Locomotive
children.delete_if { |p| !include_page?(p) }
end
- # Determines whether or not a page should be a part of the menu
+ # Determine whether or not a page should be a part of the menu.
+ #
+ # @param [ Object ] page The page
+ #
+ # @return [ Boolean ] True if the page can be included or not
+ #
def include_page?(page)
if !page.listed? || page.templatized? || !page.published?
false
@@ @@ -102,62 +99,184 @@ module Locomotive
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]}" : ''
+ # Determine wether or not a page is currently the displayed one.
+ #
+ # @param [ Object ] page The page
+ #
+ # @return [ Boolean ]
+ #
+ def page_selected?(page)
+ self.current_page.fullpath =~ /^#{page.fullpath}/
+ end
- icon = @options[:icon] ? '<span></span>' : ''
+ # Determine if the children of a page have to be rendered or not.
+ # It depends on the depth passed in the option.
+ #
+ # @param [ Object ] page The page
+ # @param [ Integer ] depth The current depth
+ #
+ # @return [ Boolean ] True if the children have to be rendered.
+ #
+ def render_children_for_page?(page, depth)
+ depth.succ <= @options[:depth].to_i &&
+ (page.children || []).select { |child| self.include_page?(child) }.any?
+ end
+ # Return the label of an entry. It may use or not the template
+ # given by the snippet option.
+ #
+ # @param [ Object ] page The page
+ #
+ # @return [ String ] The label in HTML
+ #
+ def entry_label(page)
+ 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>}
+ if icon.blank?
+ title
+ elsif @options[:icon] == 'after'
+ "#{title} #{icon}"
+ else
+ "#{icon} #{title}"
end
+ 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>}
+ # Return the localized url of an entry (page).
+ #
+ # @param [ Object ] page The page
+ #
+ # @return [ String ] The localized url
+ #
+ def entry_url(page)
+ if ::I18n.locale.to_s == self.mounting_point.default_locale.to_s
+ "/#{page.fullpath}"
+ else
+ "/#{::I18n.locale}/#{page.fullpath}"
+ end
+ end
- output.strip
+ # Return the css of an entry (page).
+ #
+ # @param [ Object ] page The page
+ # @param [ String ] css The extra css
+ #
+ # @return [ String ] The css
+ #
+ def entry_css(page, css = '')
+ _css = 'link'
+ _css = "#{page}#{@options[:active_class]}" if self.page_selected?(page)
+
+ (_css + " #{css}").strip
end
- def render_children_for_page?(page, depth)
- depth.succ <= @options[:depth].to_i && page.children.reject { |c| !include_page?(c) }.any?
+ # Return the HTML output of a page and its children if requested.
+ #
+ # @param [ Object ] page The page
+ # @param [ String ] css The current css to apply to the entry
+ # @param [ Integer] depth Used to know if the children has to be added or not.
+ #
+ # @return [ String ] The HTML output
+ #
+ def render_entry_link(page, css, depth)
+ url = self.entry_url(page)
+ label = self.entry_label(page)
+ css = self.entry_css(page, css)
+ options = ''
+
+ if self.render_children_for_page?(page, depth) && self.bootstrap?
+ url = '#'
+ label += %{ <b class="caret"></b>}
+ css += ' dropdown'
+ options = %{ class="dropdown-toggle" data-toggle="dropdown"}
+ end
+
+ self.render_tag(:li, id: "#{page.slug.to_s.dasherize}-link", css: css) do
+ children_output = depth.succ <= @options[:depth].to_i ? self.render_entry_children(page, depth.succ) : ''
+ %{<a href="#{url}"#{options}>#{label}</a>} + children_output
+ end
end
- # Recursively creates a nested unordered list for the depth specified
+ # Recursively create a nested unordered list for the depth specified.
+ #
+ # @param [ Array ] entries The children of the page
+ # @param [ Integer ] depth The current depth
+ #
+ # @return [ String ] The HTML code
+ #
def render_entry_children(page, depth)
- output = %{}
+ entries = (page.children || []).select { |child| self.include_page?(child) }
+ css = self.bootstrap? ? 'dropdown-menu' : ''
+
+ unless entries.empty?
+ self.render_tag(:ul, id: "#{@options[:id]}-#{page.slug.to_s.dasherize}", css: css) do
+ self.build_entries_output(entries, depth)
+ end
+ else
+ ''
+ end
+ end
- 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
+ # Set the value (default or assigned by the tag) of the options.
+ #
+ def set_options(markup, context)
+ @options = { id: 'nav', class: '', active_class: 'on', bootstrap: false, no_wrapper: false }
- output << render_entry_link(c, css.join(' '),depth)
+ 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
- output << %{</ul>}
end
+ end
+
+ # Avoid to call context.registers to get the current page
+ # and the mounting point.
+ #
+ def set_accessors_from_context(context)
+ self.current_page = context.registers[:page]
+ self.mounting_point = context.registers[:mounting_point]
+ end
+
+ # Parse the template of the snippet give in option of the tag.
+ # If the template_name contains a liquid tag or drop, it will
+ # be used an inline template.
+ #
+ 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
- output
+ # Render any kind HTML tags. The content of the tag comes from
+ # the block.
+ #
+ # @param [ String ] tag_name Name of the HTML tag (li, ul, div, ...etc).
+ # @param [ String ] html_options Id, class, ..etc
+ #
+ # @return [ String ] The HTML
+ #
+ def render_tag(tag_name, html_options = {}, &block)
+ options = ['']
+ options << %{id="#{html_options[:id]}"} if html_options[:id].present?
+ options << %{class="#{html_options[:css]}"} if html_options[:css].present?
+
+ %{<#{tag_name}#{options.join(' ')}>#{yield}</#{tag_name}>}
end
def bootstrap?
- @options[:bootstrap] == 'true' || @options[:bootstrap] == true
+ @options[:bootstrap].to_bool
+ end
+
+ def no_wrapper?
+ @options[:no_wrapper].to_bool
end
::Liquid::Template.register_tag('nav', Nav)
locomotive/wagon/misc/core_ext.rb b/lib/locomotive/wagon/misc/core_ext.rb +18 -0
@@ @@ -26,4 +26,22 @@ unless Hash.instance_methods.include?(:underscore_keys)
end
end
+ end
+
+ unless String.instance_methods.include?(:to_bool)
+ class String
+ def to_bool
+ return true if self == true || self =~ (/(true|t|yes|y|1)$/i)
+ return false if self == false || self.blank? || self =~ (/(false|f|no|n|0)$/i)
+ raise ArgumentError.new("invalid value for Boolean: \"#{self}\"")
+ end
+ end
+
+ class TrueClass
+ def to_bool; self; end
+ end
+
+ class FalseClass
+ def to_bool; self; end
+ end
end
\ No newline at end of file
spec/fixtures/default/app/views/pages/about_us/john_doe.liquid.haml +2 -0
@@ @@ -1,4 +1,6 @@
---
position: 1
+ published: true
+ listed: true
---
{% extends parent %}
\ No newline at end of file
spec/fixtures/default/app/views/pages/archives/news.liquid.haml +3 -0
@@ @@ -1,3 +1,6 @@
+ ---
+ title: News archive
+ ---
{% extends parent %}
{% block content %}
spec/fixtures/default/app/views/pages/tags/nav_in_deep.liquid.haml +6 -0
@@ @@ -0,0 +1,6 @@
+ ---
+ title: Page to test the nav tag with a high depth
+ listed: false
+ published: false
+ ---
+ {% nav 'site', depth: 2 %}
\ No newline at end of file
spec/integration/server/basic_spec.rb +16 -9
@@ @@ -40,7 +40,7 @@ describe Locomotive::Wagon::Server do
get '/nb'
last_response.body.should_not =~ /Powered by/
end
-
+
it 'provides translation in scopes' do
get '/'
last_response.body.should =~ /scoped_translation=.French./
@@ @@ -61,15 +61,15 @@ describe Locomotive::Wagon::Server do
it { should_not match(/<nav id="nav">/) }
- it { should match(/<li id="about-us-link" class="link first "><a href="\/about-us" >About Us <\/a><\/li>/) }
+ it { should match(/<li id="about-us-link" class="link first"><a href="\/about-us">About Us<\/a><\/li>/) }
- it { should match(/<li id="music-link" class="link "><a href="\/music" >Music <\/a><\/li>/) }
+ it { should match(/<li id="music-link" class="link"><a href="\/music">Music<\/a><\/li>/) }
- it { should match(/<li id="store-link" class="link "><a href="\/store" >Store <\/a><\/li>/) }
+ it { should match(/<li id="store-link" class="link"><a href="\/store">Store<\/a><\/li>/) }
- it { should match(/<li id="contact-link" class="link last "><a href="\/contact" >Contact Us <\/a><\/li>/) }
+ it { should match(/<li id="contact-link" class="link last"><a href="\/contact">Contact Us<\/a><\/li>/) }
- it { should_not match(/<li id="events-link" class="link "><a href="\/events" >Events <\/a><\/li>/) }
+ it { should_not match(/<li id="events-link" class="link"><a href="\/events">Events<\/a><\/li>/) }
describe 'with wrapper' do
@@ @@ -79,16 +79,23 @@ describe Locomotive::Wagon::Server do
end
+ describe 'very deep' do
+
+ subject { get '/tags/nav_in_deep'; last_response.body }
+
+ it { should match(/<li id=\"john-doe-link\" class=\"link first last\">/) }
+
+ end
end
-
+
describe 'contents with_scope' do
subject { get '/grunge_bands'; last_response.body }
-
+
it { should match(/Layne/)}
it { should_not match(/Peter/) }
end
-
+
describe "pages with_scope" do
subject { get '/unlisted_pages'; last_response.body }
it { subject.should match(/Page to test the nav tag/)}