nesta 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/.gitignore +13 -0
  2. data/Gemfile +6 -0
  3. data/Gemfile.lock +58 -0
  4. data/LICENSE +19 -0
  5. data/README.md +45 -0
  6. data/Rakefile +12 -0
  7. data/bin/nesta +67 -0
  8. data/config.ru +9 -0
  9. data/config/config.yml.sample +73 -0
  10. data/config/deploy.rb.sample +62 -0
  11. data/lib/nesta/app.rb +199 -0
  12. data/lib/nesta/cache.rb +139 -0
  13. data/lib/nesta/commands.rb +135 -0
  14. data/lib/nesta/config.rb +87 -0
  15. data/lib/nesta/models.rb +313 -0
  16. data/lib/nesta/nesta.rb +0 -0
  17. data/lib/nesta/overrides.rb +59 -0
  18. data/lib/nesta/path.rb +11 -0
  19. data/lib/nesta/plugins.rb +15 -0
  20. data/lib/nesta/version.rb +3 -0
  21. data/nesta.gemspec +49 -0
  22. data/scripts/import-from-mephisto +207 -0
  23. data/spec/atom_spec.rb +138 -0
  24. data/spec/commands_spec.rb +220 -0
  25. data/spec/config_spec.rb +69 -0
  26. data/spec/model_factory.rb +94 -0
  27. data/spec/models_spec.rb +445 -0
  28. data/spec/overrides_spec.rb +113 -0
  29. data/spec/page_spec.rb +428 -0
  30. data/spec/path_spec.rb +28 -0
  31. data/spec/sitemap_spec.rb +102 -0
  32. data/spec/spec.opts +1 -0
  33. data/spec/spec_helper.rb +72 -0
  34. data/templates/Gemfile +8 -0
  35. data/templates/Rakefile +35 -0
  36. data/templates/config.ru +9 -0
  37. data/templates/config/config.yml +73 -0
  38. data/templates/config/deploy.rb +47 -0
  39. data/views/analytics.haml +12 -0
  40. data/views/atom.builder +28 -0
  41. data/views/categories.haml +3 -0
  42. data/views/comments.haml +8 -0
  43. data/views/error.haml +13 -0
  44. data/views/feed.haml +3 -0
  45. data/views/index.haml +5 -0
  46. data/views/layout.haml +27 -0
  47. data/views/master.sass +246 -0
  48. data/views/not_found.haml +13 -0
  49. data/views/page.haml +29 -0
  50. data/views/sidebar.haml +3 -0
  51. data/views/sitemap.builder +15 -0
  52. data/views/summaries.haml +14 -0
  53. metadata +302 -0
@@ -0,0 +1,139 @@
1
+ require 'fileutils'
2
+ # require 'sinatra/base'
3
+
4
+ module Sinatra
5
+
6
+ # Sinatra Caching module
7
+ #
8
+ # TODO:: Need to write documentation here
9
+ #
10
+ module Cache
11
+
12
+ VERSION = 'Sinatra::Cache v0.2.0'
13
+ def self.version; VERSION; end
14
+
15
+
16
+ module Helpers
17
+
18
+ # Caches the given URI to a html file in /public
19
+ #
20
+ # <b>Usage:</b>
21
+ # >> cache( erb(:contact, :layout => :layout))
22
+ # => returns the HTML output written to /public/<CACHE_DIR_PATH>/contact.html
23
+ #
24
+ # Also accepts an Options Hash, with the following options:
25
+ # * :extension => in case you need to change the file extension
26
+ #
27
+ # TODO:: implement the opts={} hash functionality. What other options are needed?
28
+ #
29
+ def cache(content, opts={})
30
+ return content unless options.cache_enabled
31
+
32
+ unless content.nil?
33
+ content = "#{content}\n#{page_cached_timestamp}\n"
34
+ path = cache_page_path(request.path_info,opts)
35
+ FileUtils.makedirs(File.dirname(path))
36
+ open(path, 'wb+') { |f| f << content }
37
+ log("Cached Page: [#{path}]",:info)
38
+ content
39
+ end
40
+ end
41
+
42
+ # Expires the cached URI (as .html file) in /public
43
+ #
44
+ # <b>Usage:</b>
45
+ # >> cache_expire('/contact')
46
+ # => deletes the /public/<CACHE_DIR_PATH>contact.html page
47
+ #
48
+ # get '/contact' do
49
+ # cache_expire # deletes the /public/<CACHE_DIR_PATH>contact.html page as well
50
+ # end
51
+ #
52
+ # TODO:: implement the options={} hash functionality. What options are really needed ?
53
+ def cache_expire(path = nil, opts={})
54
+ return unless options.cache_enabled
55
+
56
+ path = (path.nil?) ? cache_page_path(request.path_info) : cache_page_path(path)
57
+ if File.exist?(path)
58
+ File.delete(path)
59
+ log("Expired Page deleted at: [#{path}]",:info)
60
+ else
61
+ log("No Expired Page was found at the path: [#{path}]",:info)
62
+ end
63
+ end
64
+
65
+ # Prints a basic HTML comment with a timestamp in it, so that you can see when a file was cached last.
66
+ #
67
+ # *NB!* IE6 does NOT like this to be the first line of a HTML document, so output
68
+ # inside the <head> tag. Many hours wasted on that lesson ;-)
69
+ #
70
+ # <b>Usage:</b>
71
+ # >> <%= page_cached_timestamp %>
72
+ # => <!-- page cached: 2009-02-24 12:00:00 -->
73
+ #
74
+ def page_cached_timestamp
75
+ "<!-- page cached: #{Time.now.strftime("%Y-%d-%m %H:%M:%S")} -->\n" if options.cache_enabled
76
+ end
77
+
78
+
79
+ private
80
+
81
+ # Establishes the file name of the cached file from the path given
82
+ #
83
+ # TODO:: implement the opts={} functionality, and support for custom extensions on a per request basis.
84
+ #
85
+ def cache_file_name(path,opts={})
86
+ name = (path.empty? || path == "/") ? "index" : Rack::Utils.unescape(path.sub(/^(\/)/,'').chomp('/'))
87
+ name << options.cache_page_extension unless (name.split('/').last || name).include? '.'
88
+ return name
89
+ end
90
+
91
+ # Sets the full path to the cached page/file
92
+ # Dependent upon Sinatra.options .public and .cache_dir variables being present and set.
93
+ #
94
+ def cache_page_path(path,opts={})
95
+ # test if given a full path rather than relative path, otherwise join the public path to cache_dir
96
+ # and ensure it is a full path
97
+ cache_dir = (options.cache_dir == File.expand_path(options.cache_dir)) ?
98
+ options.cache_dir : File.expand_path("#{options.public}/#{options.cache_dir}")
99
+ cache_dir = cache_dir[0..-2] if cache_dir[-1,1] == '/'
100
+ "#{cache_dir}/#{cache_file_name(path,opts)}"
101
+ end
102
+
103
+ # TODO:: this implementation really stinks, how do I incorporate Sinatra's logger??
104
+ def log(msg,scope=:debug)
105
+ if options.cache_logging
106
+ "Log: msg=[#{msg}]" if scope == options.cache_logging_level
107
+ else
108
+ # just ignore the stuff...
109
+ # puts "just ignoring msg=[#{msg}] since cache_logging => [#{options.cache_logging.to_s}]"
110
+ end
111
+ end
112
+
113
+ end #/module Helpers
114
+
115
+
116
+ # Sets the default options:
117
+ #
118
+ # * +:cache_enabled+ => toggle for the cache functionality. Default is: +true+
119
+ # * +:cache_page_extension+ => sets the default extension for cached files. Default is: +.html+
120
+ # * +:cache_dir+ => sets cache directory where cached files are stored. Default is: ''(empty) == root of /public.<br>
121
+ # set to empty, since the ideal 'system/cache/' does not work with Passenger & mod_rewrite :(
122
+ # * +cache_logging+ => toggle for logging the cache calls. Default is: +true+
123
+ # * +cache_logging_level+ => sets the level of the cache logger. Default is: <tt>:info</tt>.<br>
124
+ # Options:(unused atm) [:info, :warn, :debug]
125
+ #
126
+ def self.registered(app)
127
+ app.helpers(Cache::Helpers)
128
+ app.set :cache_enabled, true
129
+ app.set :cache_page_extension, '.html'
130
+ app.set :cache_dir, ''
131
+ app.set :cache_logging, true
132
+ app.set :cache_logging_level, :info
133
+ end
134
+
135
+ end #/module Cache
136
+
137
+ register(Sinatra::Cache)
138
+
139
+ end #/module Sinatra
@@ -0,0 +1,135 @@
1
+ require 'erb'
2
+ require 'fileutils'
3
+
4
+ require File.expand_path('app', File.dirname(__FILE__))
5
+ require File.expand_path('path', File.dirname(__FILE__))
6
+ require File.expand_path('version', File.dirname(__FILE__))
7
+
8
+ module Nesta
9
+ module Commands
10
+ class UsageError < RuntimeError; end
11
+
12
+ module Command
13
+ def fail(message)
14
+ $stderr.puts "Error: #{message}"
15
+ exit 1
16
+ end
17
+
18
+ def template_root
19
+ File.expand_path('../../templates', File.dirname(__FILE__))
20
+ end
21
+
22
+ def copy_template(src, dest)
23
+ FileUtils.mkdir_p(File.dirname(dest))
24
+ template = ERB.new(File.read(File.join(template_root, src)))
25
+ File.open(dest, 'w') { |file| file.puts template.result(binding) }
26
+ end
27
+
28
+ def copy_templates(templates)
29
+ templates.each { |src, dest| copy_template(src, dest) }
30
+ end
31
+ end
32
+
33
+ class New
34
+ include Command
35
+
36
+ def initialize(path, options = {})
37
+ path.nil? && (raise UsageError.new('path not specified'))
38
+ fail("#{path} already exists") if File.exist?(path)
39
+ @path = path
40
+ @options = options
41
+ end
42
+
43
+ def make_directories
44
+ %w[content/attachments content/pages].each do |dir|
45
+ FileUtils.mkdir_p(File.join(@path, dir))
46
+ end
47
+ end
48
+
49
+ def have_rake_tasks?
50
+ @options['heroku'] || @options['vlad']
51
+ end
52
+
53
+ def execute
54
+ make_directories
55
+ templates = {
56
+ 'config.ru' => "#{@path}/config.ru",
57
+ 'config/config.yml' => "#{@path}/config/config.yml",
58
+ 'Gemfile' => "#{@path}/Gemfile"
59
+ }
60
+ templates['Rakefile'] = "#{@path}/Rakefile" if have_rake_tasks?
61
+ if @options['vlad']
62
+ templates['config/deploy.rb'] = "#{@path}/config/deploy.rb"
63
+ end
64
+ copy_templates(templates)
65
+ end
66
+ end
67
+
68
+ module Theme
69
+ class Create
70
+ include Command
71
+
72
+ def initialize(name, options = {})
73
+ name.nil? && (raise UsageError.new('name not specified'))
74
+ @name = name
75
+ @theme_path = Nesta::Path.themes(@name)
76
+ fail("#{@theme_path} already exists") if File.exist?(@theme_path)
77
+ end
78
+
79
+ def make_directories
80
+ FileUtils.mkdir_p(File.join(@theme_path, 'public', @name))
81
+ FileUtils.mkdir_p(File.join(@theme_path, 'views'))
82
+ end
83
+
84
+ def execute
85
+ make_directories
86
+ copy_templates(
87
+ 'themes/README.md' => "#{@theme_path}/README.md",
88
+ 'themes/app.rb' => "#{@theme_path}/app.rb"
89
+ )
90
+ end
91
+ end
92
+
93
+ class Install
94
+ include Command
95
+
96
+ def initialize(url, options = {})
97
+ url.nil? && (raise UsageError.new('URL not specified'))
98
+ @url = url
99
+ @name = File.basename(url, '.git').sub(/nesta-theme-/, '')
100
+ end
101
+
102
+ def execute
103
+ system('git', 'clone', @url, "themes/#{@name}")
104
+ FileUtils.rm_r(File.join("themes/#{@name}", '.git'))
105
+ enable(@name)
106
+ end
107
+ end
108
+
109
+ class Enable
110
+ include Command
111
+
112
+ def initialize(name, options = {})
113
+ name.nil? && (raise UsageError.new('name not specified'))
114
+ @name = name
115
+ end
116
+
117
+ def execute
118
+ theme_config = /^\s*#?\s*theme:.*/
119
+ configured = false
120
+ File.open(Nesta::Config.yaml_path, 'r+') do |file|
121
+ output = ''
122
+ file.each_line do |line|
123
+ output << line.sub(theme_config, "theme: #{@name}")
124
+ configured = true if line =~ theme_config
125
+ end
126
+ output << "theme: #{@name}\n" unless configured
127
+ file.pos = 0
128
+ file.print(output)
129
+ file.truncate(file.pos)
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,87 @@
1
+ require "yaml"
2
+
3
+ require "rubygems"
4
+ require "sinatra"
5
+
6
+ module Nesta
7
+ class Config
8
+ @settings = %w[
9
+ title subtitle description keywords theme disqus_short_name
10
+ cache content google_analytics_code
11
+ ]
12
+ @author_settings = %w[name uri email]
13
+ @yaml = nil
14
+
15
+ class << self
16
+ attr_accessor :settings, :author_settings, :yaml_conf
17
+ end
18
+
19
+ def self.method_missing(method, *args)
20
+ setting = method.to_s
21
+ if settings.include?(setting)
22
+ from_environment(setting) || from_yaml(setting)
23
+ else
24
+ super
25
+ end
26
+ end
27
+
28
+ def self.author
29
+ environment_config = {}
30
+ %w[name uri email].each do |setting|
31
+ variable = "NESTA_AUTHOR__#{setting.upcase}"
32
+ ENV[variable] && environment_config[setting] = ENV[variable]
33
+ end
34
+ environment_config.empty? ? from_yaml("author") : environment_config
35
+ end
36
+
37
+ def self.content_path(basename = nil)
38
+ get_path(content, basename)
39
+ end
40
+
41
+ def self.page_path(basename = nil)
42
+ get_path(File.join(content_path, "pages"), basename)
43
+ end
44
+
45
+ def self.attachment_path(basename = nil)
46
+ get_path(File.join(content_path, "attachments"), basename)
47
+ end
48
+
49
+ def self.yaml_path
50
+ File.expand_path('config/config.yml', Nesta::App.root)
51
+ end
52
+
53
+ def self.from_environment(setting)
54
+ value = ENV["NESTA_#{setting.upcase}"]
55
+ overrides = { "true" => true, "false" => false }
56
+ overrides.has_key?(value) ? overrides[value] : value
57
+ end
58
+ private_class_method :from_environment
59
+
60
+ def self.yaml_exists?
61
+ File.exist?(yaml_path)
62
+ end
63
+ private_class_method :yaml_exists?
64
+
65
+ def self.can_use_yaml?
66
+ ENV.keys.grep(/^NESTA/).empty? && yaml_exists?
67
+ end
68
+ private_class_method :can_use_yaml?
69
+
70
+ def self.from_yaml(setting)
71
+ if can_use_yaml?
72
+ self.yaml_conf ||= YAML::load(IO.read(yaml_path))
73
+ rack_env_conf = self.yaml_conf[Nesta::App.environment.to_s]
74
+ (rack_env_conf && rack_env_conf[setting]) || self.yaml_conf[setting]
75
+ end
76
+ rescue Errno::ENOENT # config file not found
77
+ raise unless Nesta::App.environment == :test
78
+ nil
79
+ end
80
+ private_class_method :from_yaml
81
+
82
+ def self.get_path(dirname, basename)
83
+ basename.nil? ? dirname : File.join(dirname, basename)
84
+ end
85
+ private_class_method :get_path
86
+ end
87
+ end
@@ -0,0 +1,313 @@
1
+ require "time"
2
+
3
+ require "rubygems"
4
+ require "maruku"
5
+ require "redcloth"
6
+
7
+ module Nesta
8
+ class FileModel
9
+ FORMATS = [:mdown, :haml, :textile]
10
+ @@cache = {}
11
+
12
+ attr_reader :filename, :mtime
13
+
14
+ def self.model_path(basename = nil)
15
+ Nesta::Config.content_path(basename)
16
+ end
17
+
18
+ def self.find_all
19
+ file_pattern = File.join(model_path, "**", "*.{#{FORMATS.join(',')}}")
20
+ Dir.glob(file_pattern).map do |path|
21
+ relative = path.sub("#{model_path}/", "")
22
+ load(relative.sub(/\.(#{FORMATS.join('|')})/, ""))
23
+ end
24
+ end
25
+
26
+ def self.needs_loading?(path, filename)
27
+ @@cache[path].nil? || File.mtime(filename) > @@cache[path].mtime
28
+ end
29
+
30
+ def self.load(path)
31
+ FORMATS.each do |format|
32
+ filename = model_path("#{path}.#{format}")
33
+ if File.exist?(filename) && needs_loading?(path, filename)
34
+ @@cache[path] = self.new(filename)
35
+ break
36
+ end
37
+ end
38
+ @@cache[path]
39
+ end
40
+
41
+ def self.purge_cache
42
+ @@cache = {}
43
+ end
44
+
45
+ def self.deprecated(name, message)
46
+ if Nesta::App.environment != :test
47
+ $stderr.puts "DEPRECATION WARNING: #{name} is deprecated; #{message}"
48
+ end
49
+ end
50
+
51
+ def self.menu_items
52
+ deprecated("Page.menu_items", "see Menu.top_level and Menu.for_path")
53
+ Menu.top_level
54
+ end
55
+
56
+ def initialize(filename)
57
+ @filename = filename
58
+ @format = filename.split(".").last.to_sym
59
+ parse_file
60
+ @mtime = File.mtime(filename)
61
+ end
62
+
63
+ def permalink
64
+ File.basename(@filename, ".*")
65
+ end
66
+
67
+ def path
68
+ abspath.sub(/^\//, "")
69
+ end
70
+
71
+ def abspath
72
+ prefix = File.dirname(@filename).sub(Nesta::Config.page_path, "")
73
+ File.join(prefix, permalink)
74
+ end
75
+
76
+ def layout
77
+ (metadata("layout") || "layout").to_sym
78
+ end
79
+
80
+ def template
81
+ (metadata("template") || "page").to_sym
82
+ end
83
+
84
+ def to_html(scope = nil)
85
+ case @format
86
+ when :mdown
87
+ Maruku.new(markup).to_html
88
+ when :haml
89
+ Haml::Engine.new(markup).to_html(scope || Object.new)
90
+ when :textile
91
+ RedCloth.new(markup).to_html
92
+ end
93
+ end
94
+
95
+ def last_modified
96
+ @last_modified ||= File.stat(@filename).mtime
97
+ end
98
+
99
+ def description
100
+ metadata("description")
101
+ end
102
+
103
+ def keywords
104
+ metadata("keywords")
105
+ end
106
+
107
+ private
108
+ def markup
109
+ @markup
110
+ end
111
+
112
+ def metadata(key)
113
+ @metadata[key]
114
+ end
115
+
116
+ def paragraph_is_metadata(text)
117
+ text.split("\n").first =~ /^[\w ]+:/
118
+ end
119
+
120
+ def parse_file
121
+ first_para, remaining = File.open(@filename).read.split(/\r?\n\r?\n/, 2)
122
+ @metadata = {}
123
+ if paragraph_is_metadata(first_para)
124
+ @markup = remaining
125
+ for line in first_para.split("\n") do
126
+ key, value = line.split(/\s*:\s*/, 2)
127
+ @metadata[key.downcase] = value.chomp
128
+ end
129
+ else
130
+ @markup = [first_para, remaining].join("\n\n")
131
+ end
132
+ rescue Errno::ENOENT # file not found
133
+ raise Sinatra::NotFound
134
+ end
135
+ end
136
+
137
+ class Page < FileModel
138
+ def self.model_path(basename = nil)
139
+ Nesta::Config.page_path(basename)
140
+ end
141
+
142
+ def self.find_by_path(path)
143
+ load(path)
144
+ end
145
+
146
+ def self.find_articles
147
+ find_all.select do |page|
148
+ page.date && page.date < DateTime.now
149
+ end.sort { |x, y| y.date <=> x.date }
150
+ end
151
+
152
+ def ==(other)
153
+ other.respond_to?(:path) && (self.path == other.path)
154
+ end
155
+
156
+ def heading
157
+ regex = case @format
158
+ when :mdown
159
+ /^#\s*(.*)/
160
+ when :haml
161
+ /^\s*%h1\s+(.*)/
162
+ when :textile
163
+ /^\s*h1\.\s+(.*)/
164
+ end
165
+ markup =~ regex
166
+ Regexp.last_match(1)
167
+ end
168
+
169
+ def date(format = nil)
170
+ @date ||= if metadata("date")
171
+ if format == :xmlschema
172
+ Time.parse(metadata("date")).xmlschema
173
+ else
174
+ DateTime.parse(metadata("date"))
175
+ end
176
+ end
177
+ end
178
+
179
+ def atom_id
180
+ metadata("atom id")
181
+ end
182
+
183
+ def read_more
184
+ metadata("read more") || "Continue reading"
185
+ end
186
+
187
+ def summary
188
+ if summary_text = metadata("summary")
189
+ summary_text.gsub!('\n', "\n")
190
+ case @format
191
+ when :textile
192
+ RedCloth.new(summary_text).to_html
193
+ else
194
+ Maruku.new(summary_text).to_html
195
+ end
196
+ end
197
+ end
198
+
199
+ def body
200
+ case @format
201
+ when :mdown
202
+ body_text = markup.sub(/^#[^#].*$\r?\n(\r?\n)?/, "")
203
+ Maruku.new(body_text).to_html
204
+ when :haml
205
+ body_text = markup.sub(/^\s*%h1\s+.*$\r?\n(\r?\n)?/, "")
206
+ Haml::Engine.new(body_text).render
207
+ when :textile
208
+ body_text = markup.sub(/^\s*h1\.\s+.*$\r?\n(\r?\n)?/, "")
209
+ RedCloth.new(body_text).to_html
210
+ end
211
+ end
212
+
213
+ def categories
214
+ categories = metadata("categories")
215
+ paths = categories.nil? ? [] : categories.split(",").map { |p| p.strip }
216
+ valid_paths(paths).map { |p| Page.find_by_path(p) }.sort do |x, y|
217
+ x.heading.downcase <=> y.heading.downcase
218
+ end
219
+ end
220
+
221
+ def parent
222
+ Page.load(File.dirname(path))
223
+ end
224
+
225
+ def pages
226
+ Page.find_all.select do |page|
227
+ page.date.nil? && page.categories.include?(self)
228
+ end.sort do |x, y|
229
+ x.heading.downcase <=> y.heading.downcase
230
+ end
231
+ end
232
+
233
+ def articles
234
+ Page.find_articles.select { |article| article.categories.include?(self) }
235
+ end
236
+
237
+ private
238
+ def valid_paths(paths)
239
+ paths.select do |path|
240
+ FORMATS.detect do |format|
241
+ File.exist?(File.join(Nesta::Config.page_path, "#{path}.#{format}"))
242
+ end
243
+ end
244
+ end
245
+ end
246
+
247
+ class Menu
248
+ INDENT = " " * 2
249
+
250
+ def self.full_menu
251
+ menu = []
252
+ menu_file = Nesta::Config.content_path('menu.txt')
253
+ if File.exist?(menu_file)
254
+ File.open(menu_file) { |file| append_menu_item(menu, file, 0) }
255
+ end
256
+ menu
257
+ end
258
+
259
+ def self.top_level
260
+ full_menu.reject { |item| item.is_a?(Array) }
261
+ end
262
+
263
+ def self.for_path(path)
264
+ path.sub!(Regexp.new('^/'), '')
265
+ if path.empty?
266
+ full_menu
267
+ else
268
+ find_menu_item_by_path(full_menu, path)
269
+ end
270
+ end
271
+
272
+ private
273
+ def self.append_menu_item(menu, file, depth)
274
+ path = file.readline
275
+ rescue EOFError
276
+ else
277
+ page = Page.load(path.strip)
278
+ if page
279
+ current_depth = path.scan(INDENT).size
280
+ if current_depth > depth
281
+ sub_menu_for_depth(menu, depth) << [page]
282
+ else
283
+ sub_menu_for_depth(menu, current_depth) << page
284
+ end
285
+ append_menu_item(menu, file, current_depth)
286
+ end
287
+ end
288
+
289
+ def self.sub_menu_for_depth(menu, depth)
290
+ sub_menu = menu
291
+ depth.times { sub_menu = sub_menu[-1] }
292
+ sub_menu
293
+ end
294
+
295
+ def self.find_menu_item_by_path(menu, path)
296
+ item = menu.detect do |item|
297
+ item.respond_to?(:path) && (item.path == path)
298
+ end
299
+ if item
300
+ subsequent = menu[menu.index(item) + 1]
301
+ item = [item]
302
+ item << subsequent if subsequent.respond_to?(:each)
303
+ else
304
+ sub_menus = menu.select { |menu_item| menu_item.respond_to?(:each) }
305
+ sub_menus.each do |sub_menu|
306
+ item = find_menu_item_by_path(sub_menu, path)
307
+ break if item
308
+ end
309
+ end
310
+ item
311
+ end
312
+ end
313
+ end