How to build nested menu "trees" in HAML - ruby

I am trying to build a simple nested html menu using HAML and am not sure how to go about inserting the elements with the correct indentation, or the general best way to build nested trees. I would like to be able to do something like this, but infinitely deep:
- categories.each_key do |category|
%li.cat-item{:id => "category-#{category}"}
%a{:href => "/category/#{category}", :title => "#{category.titleize}"}
= category.titleize
It feels like I should be able to accomplish this pretty easily without resorting to writing the tags by hand in html, but I'm not the best with recursion. Here is the code I've currently come up with:
View Helper
def menu_tag_builder(array, &block)
return "" if array.nil?
result = "<ul>\n"
array.each do |node|
result += "<li"
attributes = {}
if block_given?
text = yield(attributes, node)
else
text = node["title"]
end
attributes.each { |k,v| result += " #{k.to_s}='#{v.to_s}'"}
result += ">\n"
result += text
result += menu_tag_builder(node["children"], &block)
result += "</li>\n"
end
result += "</ul>"
result
end
def menu_tag(array, &block)
haml_concat(menu_tag_builder(array, &block))
end
View
# index.haml, where config(:menu) converts the yaml below
# to an array of objects, where object[:children] is a nested array
- menu_tag(config(:menu)) do |attributes, node|
- attributes[:class] = "one two"
- node["title"]
Sample YAML defining Menu
menu:
-
title: "Home"
path: "/home"
-
title: "About Us"
path: "/about"
children:
-
title: "Our Story"
path: "/about/our-story"
Any ideas how to do that so the output is like this:
<ul>
<li class='one two'>
Home
</li>
<li class='one two'>
About Us
</li>
</ul>
...not like this:
<ul>
<li class='one two'>
Home</li>
<li class='one two'>
About Us</li>
</ul>
... and so it's properly indented globally.
Thanks for the help,
Lance

The trick to nicely-indented, Ruby-generated Haml code is the haml_tag helper. Here's how I'd convert your menu_tag method to using haml_tag:
def menu_tag(array, &block)
return unless array
haml_tag :ul do
array.each do |node|
attributes = {}
if block_given?
text = yield(attributes, node)
else
text = node["title"]
end
haml_tag :li, text, attributes
menu_tag_builder(node["children"], &block)
end
end
end

How about something along the lines of:
def nested_list(list)
return unless list
haml_tag :ul do
list.each do |item|
haml_tag :li do
haml_concat link_to item["title"], item["path"]
if item["children"]
nested_list item["children"]
end
end
end
end
end

Awesome, #shingara's hint put me in the right direction :). This works perfectly:
def menu_tag(array, &block)
return "" if array.nil?
haml_tag :ui do
array.each do |node|
attributes = {}
if block_given?
text = yield(attributes, node)
else
text = node[:title]
end
haml_tag :li, attributes do
haml_concat text
menu_tag_builder(node[:children], &block)
end
end
end
end
If somebody can make that even shorter, or make it more easy to customize the attributes on the nested nodes, I'll mark that as correct instead of this.
Cheers.

It's because you send a pur HTML by your helper. The indentation become with HAML. You can can generate some HAML in your helper.

Related

Use a here string and lambda in a Sinatra helper method

I'm trying to create a Sinatra helper that returns a dynamically-generated HTML. I thought I would use a here string for the static bit and a lambda to calculate the dynamic part.
foo_helper.rb:
require 'erb'
module FooHelper
def tabs(selected)
template = ERB.new <<~HERE
<ul class="nav nav-tabs">
<li class="nav-item"><a class="nav-link <%= 'active' if selected == 'favorites' %>" href="/foo/favorites">Favorites</a></li>
<li class="nav-item"><a class="nav-link <%= 'active' if selected == 'all' %>" href="/foo">All</a></li>
<%= alpha.call %>
</ul>
HERE
# binding to a string works as expected
# alpha = "<li class='nav-item'><a class='nav-link' href='/foo/a'>A</a></li>"
# binding to a lambda, doesn't
alpha = lambda {
('a'..'z').each do |letter|
"<li class='nav-item'><a class='nav-link #{ 'active' if selected == letter }' href='/foo/#{letter}'>#{letter}</a></li>"
end
}
template.result(binding)
end
end
foo_controller.rb:
class FooController < ApplicationController
helpers FooHelper if defined?(FooHelper)
...
end
index.erb:
...
<%= tabs('favorites') %>
...
Results:
Displays the range, rather than the individual lis.
Am I missing something in the lambda?
** edit **
Corrected the numerous errors.
There are just too many mistakes in your code.
#nav is defined at module level, but accessed at instance level, so you got nil when you need it.
When you call a lambda, you need a dot between the variable name and the opening parenthesis, like foo.(123)
#nav.foo(binding), really? What's the (supposed) type of #nav? Does that type have the instance method foo?
<%= foo %> won't execute foo, because it's a local variable, not a method.
The reason why it renders a..z is because the Range#each method (called from within the lambda) executes a given block for each element and then returns the range itself again.
What you would want to use here instead is the Enumerable#map method. Similar to #each it also executes a block for each element, but the return values of said block are returned in a new array.
For comparison:
p ("a".."c").each { |x| x.upcase }
#=> "a".."c"
p ("a".."c").map { |x| x.upcase }
#=> ["A", "B", "C"]

Ruby Conditional argument to method

I have some 'generic' methods that extract data based on css selectors that usually are the same in many websites. However I have another method that accept as argument the css selector for a given website.
I need to call the get_title method if title_selector argument is nos passed. How can I do that?
Scrape that accept css selectors as arguments
def scrape(urls, item_selector, title_selector, price_selector, image_selector)
collection = []
urls.each do |url|
doc = Nokogiri::HTML(open(url).read) # Opens URL
#items = doc.css(item_selector)[0..1].map {|item| item['href']} # Sets items
#items.each do |item| # Donwload each link and parse
page = Nokogiri::HTML(open(item).read)
collection << {
:title => page.css(title_selector).text, # I guess I need conditional here
:price => page.css(price_selector).text
}
end
#collection = collection
end
end
Generic title extractor
def get_title(doc)
if doc.at_css("meta[property='og:title']")
title = doc.css("meta[property='og:title']")
else doc.css('title')
title = doc.at_css('title').text
end
end
Use an or operator inside your page.css call. It will call get_title if title_selector is falsey (nil).
:title => page.css(title_selector || get_title(doc)).text,
I'm not sure what doc should actually be in this context, though.
EDIT
Given your comment below, I think you can just refactor get_title to handle all of the logic. Allow get_title to take an optional title_selector parameter and add this line to the top of your method:
return doc.css(title_selector).text if title_selector
Then, my original line becomes:
:title => get_title(page, title_selector)

HAML - if / elsif construction

I need this construction in my HAML code:
- if something1
%div.a
- elsif something2
%div.b
- elsif something3
%div.c
- else
%div.d
%div another content
I would expected I get something like:
<div class="a|b|c|d">
<div>another content</div>
</div>
But in the fact I get
<div class="a|b|c|d"></div>
<div>another content</div>
How I must to update my code, if I need to get:
another content
?
I think you should create a helper method instead:
%div{:class => helper_method(useful_parameters)}
The really ugly way to accomplish this is with ternary operators (condition ? true_case : false_case) which doesn't sound like a good solution given from the fact that you selected haml and want to have your code base clean.
#Ingenu's helper method looks like the smarter approach, but if you don't mind it quicker and dirtier, you could do:
- if something1
-divclass = 'a'
- elsif something2
-divclass = 'b'
- elsif something3
-divclass = 'c'
- else
-divclass = 'd'
%div{:class => divclass}
%div another content
You could extend your if condition with this module and then use smalltalk-style conditions
module IfTrue
def ifTrue &block
yield if self
slf=self
o = Object.new
def o.elseIf val, &block
yield if !slf && val
end
end
end
now you could code stuff like this:
condition.extend(IfTrue).ifTrue{
do_stuff
}elseIf(condition2){
doOtherStuff
}
or, if you are a naughty monkey patcher ;-):
Object.include IfTrue
condition.ifTrue{
do_stuff
}elseIf(condition2){
doOtherStuff
}
if you want to chain more than one elseif you will have to adapt this code by somehow factoring the elsif definition

Ruby, better way to implement conditional iteration than this?

I have an array #cities = ["Vienna", "Barcelona", "Paris"];
and I am trying to display the individual items with a spacer in between. However it is possible that there is only 1 element in the array, in which case I do not want to display the spacer. And also the array could be empty, in which case I want to display nothing.
For the above array I want the following output:
Vienna
-----
Barcelona
-----
Paris
I use an erb template cityview to apply formatting, css, etc before actually printing the city names. Simplified, it looks like this:
<p><%= #cities[#city_id] %></p>
I have implemented it as follows...
unless #array.empty?
#city_id = 0;
erb :cityview
end
unless #array[1..-1].nil?
#array[1..-1].each_index do |i|
#city_id = i+1;
puts "<p>-------</p>";
erb :cityview
end
end
Is there a better way?
#cities.join("<p>--------</p>")
Edit to address the template
Here I'm assuming that there's an erbs method that returns the rendered template without doing a puts. Returning the string allows easier manipulation and reuse.
#cities.map { |c| #city = c; erb :cityview }.join("<p>--------</p>")
I'd prefer:
erb:
<p><%= #city %></p>
and loop
#array.each_with_index do |e, i|
#city = e
erb :cityview
puts "<p>-------</p>" if i < #array.length - 1
end
I assume you have split the erb, bit because you want to customize it.
If you want to mix HTML with your city names then you'll need to worry about HTML encoding things before you mix in your HTML. Using just the standard library:
require 'cgi'
html = #cities.map { |c| CGI.escapeHTML(c) }.join('<p>-----</p>')
If you're in Rails, then you can use html_escape from ERB::Util and mark the result as safe-for-HTML with html_safe to avoid having to worry about the encoding in your view:
include ERB::Util
html = #cities.map { |c| html_escape(c) }.join('<p>-----</p>').html_safe
The simpler solution would be to use a spacer template.
http://guides.rubyonrails.org/layouts_and_rendering.html#spacer-templates

How to build, sort and print a tree of a sort?

This is more of an algorithmic dilemma than a language-specific problem, but since I'm currently using Ruby I'll tag this as such. I've already spent over 20 hours on this and I would've never believed it if someone told me writing a LaTeX parser was a walk in the park in comparison.
I have a loop to read hierarchies (that are prefixed with \m) from different files
art.tex: \m{Art}
graphical.tex: \m{Art}{Graphical}
me.tex: \m{About}{Me}
music.tex: \m{Art}{Music}
notes.tex: \m{Art}{Music}{Sheet Music}
site.tex: \m{About}{Site}
something.tex: \m{Something}
whatever.tex: \m{Something}{That}{Does Not}{Matter}
and I need to sort them alphabetically and print them out as a tree
About
Me (me.tex)
Site (site.tex)
Art (art.tex)
Graphical (graphical.tex)
Music (music.tex)
Sheet Music (notes.tex)
Something (something.tex)
That
Does Not
Matter (whatever.tex)
in (X)HTML
<ul>
<li>About</li>
<ul>
<li>Me</li>
<li>Site</li>
</ul>
<li>Art</li>
<ul>
<li>Graphical</li>
<li>Music</li>
<ul>
<li>Sheet Music</li>
</ul>
</ul>
<li>Something</li>
<ul>
<li>That</li>
<ul>
<li>Doesn't</li>
<ul>
<li>Matter</li>
</ul>
</ul>
</ul>
</ul>
using Ruby without Rails, which means that at least Array.sort and Dir.glob are available.
All of my attempts were formed like this (as this part should work just fine).
def fss_brace_array(ss_input)#a concise version of another function; converts {1}{2}...{n} into an array [1, 2, ..., n] or returns an empty array
ss_output = ss_input[1].scan(%r{\{(.*?)\}})
rescue
ss_output = []
ensure
return ss_output
end
#define tree
s_handle = File.join(:content.to_s, "*")
Dir.glob("#{s_handle}.tex").each do |s_handle|
File.open(s_handle, "r") do |f_handle|
while s_line = f_handle.gets
if s_all = s_line.match(%r{\\m\{(\{.*?\})+\}})
s_all = s_all.to_a
#do something with tree, fss_brace_array(s_all) and s_handle
break
end
end
end
end
#do something else with tree
Important: I can't SSH into my linux box from work right now, which means I cannot test this code. Not even the tiniest bit. It could have silly, obvious syntax errors or logic since I wrote it from scratch right in the input box. But it LOOKS right... I think. I'll check it when I get home from work.
SOURCE = <<-INPUT
art.tex: \m{Art}
graphical.tex: \m{Art}{Graphical}
me.tex: \m{About}{Me}
music.tex: \m{Art}{Music}
notes.tex: \m{Art}{Music}{Sheet Music}
site.tex: \m{About}{Site}
something.tex: \m{Something}
whatever.tex: \m{Something}{That}{Does Not}{Matter}
INPUT
HREF = '#href'
def insert_leaves(tree,node_list)
next = node_list[0]
rest = node_list[1..-1]
tree[next] ||= {}
if not rest.empty?
insert_leaves(tree[next],rest)
else
tree[next]
# recursively, this will fall out to be the final result, making the
# function return the last (deepest) node inserted.
end
end
tree = {}
SOURCE.each_line do |line|
href, folder_string = line.split(': \\m') #=> ['art.tex','{Art}{Graphical}']
folders = folder_string.scan(/[^{}]+/) #=> ['Art','Graphical']
deepest_folder = insert_leaves(tree,folders)
deepest_folder[HREF] = href
end
# After this insertion, tree looks like this:
#
# {
# About = {
# Me = {
# #href = me.tex
# }
# Site = {
# #href = site.tex
# }
# }
# Art = {
# Graphical = {
# #href = graphical.tex
# }
# ...
#
# Edge case: No category should be named '#href'.
def recursive_html_construction(branch, html)
return if branch.keys.reject(HREF).empty? # abort if the only key is
# an href.
html << '<ul>'
branch.keys.sort.each do |category|
next if category == HREF # skip href entries.
html << '<li>'
if branch[category].key?(HREF)
html << "<a href='#{branch[category][HREF]}'> #{category}</a>"
else
html << category
end
html << '</li>'
recursive_html_construction(branch[category],html)
end
html << '</ul>'
end
html = ""
recursive_html_construction(tree,html)
puts html # => '<ul><li>About</li><ul><li><a href='me.tex'>Me</a></li><li>
# <a href='site.tex'>Site</a></li></ul><li>Art</li><ul><li>
# <a href='graphical.tex'>Graphical</a></li>...
I am not familiar with Ruby, but this is how could you do it in most languages:
Make an empty tree structure.
For each line:
For each {} element after \m:
If this element can be found in a tree at the same level, do nothing.
Otherwise create a tree node that is a child of the previous element or a root if it is the first one
At the end of the line, attach the foo.tex part the the leafmost node.
I do not know how this translates to Ruby, specifically, how tree structure is represented there.
There was a question very similar to this one recently, take a look at mine and other answers:
How to handle recursive parent/child problems like this?
Here's a working solution in Python after simply pushing all of the entries into an array.
import operator
import itertools
def overput(input):
for betweenput, output in itertools.groupby(input, key=operator.itemgetter(0)):
yield '<li>'
yield betweenput
output = [x[1:] for x in output if len(x) > 1]
if output:
yield '<ul>'
for part in overput(output):
yield part
yield '</ul>'
yield '</li>'

Resources