nesta 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,67 @@
1
+ module Nesta
2
+ class Menu
3
+ INDENT = " " * 2
4
+
5
+ def self.full_menu
6
+ menu = []
7
+ menu_file = Nesta::Config.content_path('menu.txt')
8
+ if File.exist?(menu_file)
9
+ File.open(menu_file) { |file| append_menu_item(menu, file, 0) }
10
+ end
11
+ menu
12
+ end
13
+
14
+ def self.top_level
15
+ full_menu.reject { |item| item.is_a?(Array) }
16
+ end
17
+
18
+ def self.for_path(path)
19
+ path.sub!(Regexp.new('^/'), '')
20
+ if path.empty?
21
+ full_menu
22
+ else
23
+ find_menu_item_by_path(full_menu, path)
24
+ end
25
+ end
26
+
27
+ private_class_method def self.append_menu_item(menu, file, depth)
28
+ path = file.readline
29
+ rescue EOFError
30
+ else
31
+ page = Page.load(path.strip)
32
+ current_depth = path.scan(INDENT).size
33
+ if page
34
+ if current_depth > depth
35
+ sub_menu_for_depth(menu, depth) << [page]
36
+ else
37
+ sub_menu_for_depth(menu, current_depth) << page
38
+ end
39
+ end
40
+ append_menu_item(menu, file, current_depth)
41
+ end
42
+
43
+ private_class_method def self.sub_menu_for_depth(menu, depth)
44
+ sub_menu = menu
45
+ depth.times { sub_menu = sub_menu[-1] }
46
+ sub_menu
47
+ end
48
+
49
+ private_class_method def self.find_menu_item_by_path(menu, path)
50
+ item = menu.detect do |item|
51
+ item.respond_to?(:path) && (item.path == path)
52
+ end
53
+ if item
54
+ subsequent = menu[menu.index(item) + 1]
55
+ item = [item]
56
+ item << subsequent if subsequent.respond_to?(:each)
57
+ else
58
+ sub_menus = menu.select { |menu_item| menu_item.respond_to?(:each) }
59
+ sub_menus.each do |sub_menu|
60
+ item = find_menu_item_by_path(sub_menu, path)
61
+ break if item
62
+ end
63
+ end
64
+ item
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,167 @@
1
+ module Nesta
2
+ class HeadingNotSet < RuntimeError; end
3
+ class LinkTextNotSet < RuntimeError; end
4
+
5
+ class Page < FileModel
6
+ def self.model_path(basename = nil)
7
+ Nesta::Config.page_path(basename)
8
+ end
9
+
10
+ def self.find_by_path(path)
11
+ page = load(path)
12
+ page && page.hidden? ? nil : page
13
+ end
14
+
15
+ def self.find_all
16
+ super.select { |p| ! p.hidden? }
17
+ end
18
+
19
+ def self.find_articles
20
+ find_all.select do |page|
21
+ page.date && page.date < DateTime.now
22
+ end.sort { |x, y| y.date <=> x.date }
23
+ end
24
+
25
+ def draft?
26
+ flagged_as?('draft')
27
+ end
28
+
29
+ def hidden?
30
+ draft? && Nesta::App.production?
31
+ end
32
+
33
+ def heading
34
+ regex = case @format
35
+ when :mdown, :md
36
+ /^#\s*(.*?)(\s*#+|$)/
37
+ when :haml
38
+ /^\s*%h1\s+(.*)/
39
+ when :textile
40
+ /^\s*h1\.\s+(.*)/
41
+ end
42
+ markup =~ regex
43
+ Regexp.last_match(1) or raise HeadingNotSet, "#{abspath} needs a heading"
44
+ end
45
+
46
+ def link_text
47
+ metadata('link text') || heading
48
+ rescue HeadingNotSet
49
+ raise LinkTextNotSet, "Need to link to '#{abspath}' but can't get link text"
50
+ end
51
+
52
+ def title
53
+ metadata('title') || link_text
54
+ rescue LinkTextNotSet
55
+ return Nesta::Config.title if abspath == '/'
56
+ raise
57
+ end
58
+
59
+ def date(format = nil)
60
+ @date ||= if metadata("date")
61
+ if format == :xmlschema
62
+ Time.parse(metadata("date")).xmlschema
63
+ else
64
+ DateTime.parse(metadata("date"))
65
+ end
66
+ end
67
+ end
68
+
69
+ def atom_id
70
+ metadata('atom id')
71
+ end
72
+
73
+ def read_more
74
+ metadata('read more') || Nesta::Config.read_more
75
+ end
76
+
77
+ def summary
78
+ if summary_text = metadata("summary")
79
+ summary_text.gsub!('\n', "\n")
80
+ convert_to_html(@format, Object.new, summary_text)
81
+ end
82
+ end
83
+
84
+ def body_markup
85
+ case @format
86
+ when :mdown, :md
87
+ markup.sub(/^#[^#].*$\r?\n(\r?\n)?/, '')
88
+ when :haml
89
+ markup.sub(/^\s*%h1\s+.*$\r?\n(\r?\n)?/, '')
90
+ when :textile
91
+ markup.sub(/^\s*h1\.\s+.*$\r?\n(\r?\n)?/, '')
92
+ end
93
+ end
94
+
95
+ def body(scope = Object.new)
96
+ convert_to_html(@format, scope, body_markup)
97
+ end
98
+
99
+ def categories
100
+ paths = category_strings.map { |specifier| specifier.sub(/:-?\d+$/, '') }
101
+ valid_paths(paths).map { |p| Page.find_by_path(p) }
102
+ end
103
+
104
+ def priority(category)
105
+ category_string = category_strings.detect do |string|
106
+ string =~ /^#{category}([,:\s]|$)/
107
+ end
108
+ category_string && category_string.split(':', 2)[-1].to_i
109
+ end
110
+
111
+ def parent
112
+ if abspath == '/'
113
+ nil
114
+ else
115
+ parent_path = File.dirname(path)
116
+ while parent_path != '.' do
117
+ parent = Page.load(parent_path)
118
+ return parent unless parent.nil?
119
+ parent_path = File.dirname(parent_path)
120
+ end
121
+ return categories.first unless categories.empty?
122
+ Page.load('index')
123
+ end
124
+ end
125
+
126
+ def pages
127
+ in_category = Page.find_all.select do |page|
128
+ page.date.nil? && page.categories.include?(self)
129
+ end
130
+ in_category.sort do |x, y|
131
+ by_priority = y.priority(path) <=> x.priority(path)
132
+ if by_priority == 0
133
+ x.link_text.downcase <=> y.link_text.downcase
134
+ else
135
+ by_priority
136
+ end
137
+ end
138
+ end
139
+
140
+ def articles
141
+ Page.find_articles.select { |article| article.categories.include?(self) }
142
+ end
143
+
144
+ def receives_comments?
145
+ ! date.nil?
146
+ end
147
+
148
+ private
149
+
150
+ def category_strings
151
+ strings = metadata('categories')
152
+ strings.nil? ? [] : strings.split(',').map { |string| string.strip }
153
+ end
154
+
155
+ def valid_paths(paths)
156
+ page_dir = Nesta::Config.page_path
157
+ paths.select do |path|
158
+ FORMATS.detect do |format|
159
+ [path, File.join(path, 'index')].detect do |candidate|
160
+ File.exist?(File.join(page_dir, "#{candidate}.#{format}"))
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+
167
+ end
data/lib/nesta/models.rb CHANGED
@@ -1,423 +1,5 @@
1
- require 'time'
2
-
3
1
  require 'rdiscount'
4
2
 
5
- def register_template_handler(class_name, *extensions)
6
- Tilt.register Tilt.const_get(class_name), *extensions
7
- rescue LoadError
8
- # Only one of the Markdown processors needs to be available, so we can
9
- # safely ignore these load errors.
10
- end
11
-
12
- register_template_handler :MarukuTemplate, 'mdown', 'md'
13
- register_template_handler :KramdownTemplate, 'mdown', 'md'
14
- register_template_handler :BlueClothTemplate, 'mdown', 'md'
15
- register_template_handler :RDiscountTemplate, 'mdown', 'md'
16
- register_template_handler :RedcarpetTemplate, 'mdown', 'md'
17
-
18
- module Nesta
19
- class HeadingNotSet < RuntimeError; end
20
- class LinkTextNotSet < RuntimeError; end
21
- class MetadataParseError < RuntimeError; end
22
-
23
- class FileModel
24
- FORMATS = [:mdown, :md, :haml, :textile]
25
- @@model_cache = {}
26
- @@filename_cache = {}
27
-
28
- attr_reader :filename, :mtime
29
-
30
- class CaseInsensitiveHash < Hash
31
- def [](key)
32
- super(key.to_s.downcase)
33
- end
34
- end
35
-
36
- def self.model_path(basename = nil)
37
- Nesta::Config.content_path(basename)
38
- end
39
-
40
- def self.find_all
41
- file_pattern = File.join(model_path, "**", "*.{#{FORMATS.join(',')}}")
42
- Dir.glob(file_pattern).map do |path|
43
- relative = path.sub("#{model_path}/", "")
44
- load(relative.sub(/\.(#{FORMATS.join('|')})/, ""))
45
- end
46
- end
47
-
48
- def self.find_file_for_path(path)
49
- if ! @@filename_cache.has_key?(path)
50
- FORMATS.each do |format|
51
- [path, File.join(path, 'index')].each do |basename|
52
- filename = model_path("#{basename}.#{format}")
53
- if File.exist?(filename)
54
- @@filename_cache[path] = filename
55
- break
56
- end
57
- end
58
- end
59
- end
60
- @@filename_cache[path]
61
- end
62
-
63
- def self.needs_loading?(path, filename)
64
- @@model_cache[path].nil? || File.mtime(filename) > @@model_cache[path].mtime
65
- end
66
-
67
- def self.load(path)
68
- if (filename = find_file_for_path(path)) && needs_loading?(path, filename)
69
- @@model_cache[path] = self.new(filename)
70
- end
71
- @@model_cache[path]
72
- end
73
-
74
- def self.purge_cache
75
- @@model_cache = {}
76
- @@filename_cache = {}
77
- end
78
-
79
- def initialize(filename)
80
- @filename = filename
81
- @format = filename.split('.').last.to_sym
82
- if File.zero?(filename)
83
- @metadata = {}
84
- @markup = ''
85
- else
86
- @metadata, @markup = parse_file
87
- end
88
- @mtime = File.mtime(filename)
89
- end
90
-
91
- def ==(other)
92
- other.respond_to?(:path) && (self.path == other.path)
93
- end
94
-
95
- def index_page?
96
- @filename =~ /\/?index\.\w+$/
97
- end
98
-
99
- def abspath
100
- file_path = @filename.sub(self.class.model_path, '')
101
- if index_page?
102
- File.dirname(file_path)
103
- else
104
- File.join(File.dirname(file_path), File.basename(file_path, '.*'))
105
- end
106
- end
107
-
108
- def path
109
- abspath.sub(/^\//, '')
110
- end
111
-
112
- def permalink
113
- File.basename(path)
114
- end
115
-
116
- def layout
117
- (metadata('layout') || 'layout').to_sym
118
- end
119
-
120
- def template
121
- (metadata('template') || 'page').to_sym
122
- end
123
-
124
- def to_html(scope = Object.new)
125
- convert_to_html(@format, scope, markup)
126
- end
127
-
128
- def last_modified
129
- @last_modified ||= File.stat(@filename).mtime
130
- end
131
-
132
- def description
133
- metadata('description')
134
- end
135
-
136
- def keywords
137
- metadata('keywords')
138
- end
139
-
140
- def metadata(key)
141
- @metadata[key]
142
- end
143
-
144
- def flagged_as?(flag)
145
- flags = metadata('flags')
146
- flags && flags.split(',').map { |name| name.strip }.include?(flag)
147
- end
148
-
149
- def parse_metadata(first_paragraph)
150
- is_metadata = first_paragraph.split("\n").first =~ /^[\w ]+:/
151
- raise MetadataParseError unless is_metadata
152
- metadata = CaseInsensitiveHash.new
153
- first_paragraph.split("\n").each do |line|
154
- key, value = line.split(/\s*:\s*/, 2)
155
- next if value.nil?
156
- metadata[key.downcase] = value.chomp
157
- end
158
- metadata
159
- end
160
-
161
- private
162
- def markup
163
- @markup
164
- end
165
-
166
- def parse_file
167
- contents = File.open(@filename).read
168
- rescue Errno::ENOENT
169
- raise Sinatra::NotFound
170
- else
171
- first_paragraph, remaining = contents.split(/\r?\n\r?\n/, 2)
172
- begin
173
- return parse_metadata(first_paragraph), remaining
174
- rescue MetadataParseError
175
- return {}, contents
176
- end
177
- end
178
-
179
- def add_p_tags_to_haml(text)
180
- contains_tags = (text =~ /^\s*%/)
181
- if contains_tags
182
- text
183
- else
184
- text.split(/\r?\n/).inject('') do |accumulator, line|
185
- accumulator << "%p #{line}\n"
186
- end
187
- end
188
- end
189
-
190
- def convert_to_html(format, scope, text)
191
- text = add_p_tags_to_haml(text) if @format == :haml
192
- template = Tilt[format].new { text }
193
- template.render(scope)
194
- end
195
- end
196
-
197
- class Page < FileModel
198
- def self.model_path(basename = nil)
199
- Nesta::Config.page_path(basename)
200
- end
201
-
202
- def self.find_by_path(path)
203
- page = load(path)
204
- page && page.hidden? ? nil : page
205
- end
206
-
207
- def self.find_all
208
- super.select { |p| ! p.hidden? }
209
- end
210
-
211
- def self.find_articles
212
- find_all.select do |page|
213
- page.date && page.date < DateTime.now
214
- end.sort { |x, y| y.date <=> x.date }
215
- end
216
-
217
- def draft?
218
- flagged_as?('draft')
219
- end
220
-
221
- def hidden?
222
- draft? && Nesta::App.production?
223
- end
224
-
225
- def heading
226
- regex = case @format
227
- when :mdown, :md
228
- /^#\s*(.*?)(\s*#+|$)/
229
- when :haml
230
- /^\s*%h1\s+(.*)/
231
- when :textile
232
- /^\s*h1\.\s+(.*)/
233
- end
234
- markup =~ regex
235
- Regexp.last_match(1) or raise HeadingNotSet, "#{abspath} needs a heading"
236
- end
237
-
238
- def link_text
239
- metadata('link text') || heading
240
- rescue HeadingNotSet
241
- raise LinkTextNotSet, "Need to link to '#{abspath}' but can't get link text"
242
- end
243
-
244
- def title
245
- metadata('title') || link_text
246
- rescue LinkTextNotSet
247
- return Nesta::Config.title if abspath == '/'
248
- raise
249
- end
250
-
251
- def date(format = nil)
252
- @date ||= if metadata("date")
253
- if format == :xmlschema
254
- Time.parse(metadata("date")).xmlschema
255
- else
256
- DateTime.parse(metadata("date"))
257
- end
258
- end
259
- end
260
-
261
- def atom_id
262
- metadata('atom id')
263
- end
264
-
265
- def read_more
266
- metadata('read more') || Nesta::Config.read_more
267
- end
268
-
269
- def summary
270
- if summary_text = metadata("summary")
271
- summary_text.gsub!('\n', "\n")
272
- convert_to_html(@format, Object.new, summary_text)
273
- end
274
- end
275
-
276
- def body_markup
277
- case @format
278
- when :mdown, :md
279
- markup.sub(/^#[^#].*$\r?\n(\r?\n)?/, '')
280
- when :haml
281
- markup.sub(/^\s*%h1\s+.*$\r?\n(\r?\n)?/, '')
282
- when :textile
283
- markup.sub(/^\s*h1\.\s+.*$\r?\n(\r?\n)?/, '')
284
- end
285
- end
286
-
287
- def body(scope = Object.new)
288
- convert_to_html(@format, scope, body_markup)
289
- end
290
-
291
- def categories
292
- paths = category_strings.map { |specifier| specifier.sub(/:-?\d+$/, '') }
293
- valid_paths(paths).map { |p| Page.find_by_path(p) }
294
- end
295
-
296
- def priority(category)
297
- category_string = category_strings.detect do |string|
298
- string =~ /^#{category}([,:\s]|$)/
299
- end
300
- category_string && category_string.split(':', 2)[-1].to_i
301
- end
302
-
303
- def parent
304
- if abspath == '/'
305
- nil
306
- else
307
- parent_path = File.dirname(path)
308
- while parent_path != '.' do
309
- parent = Page.load(parent_path)
310
- return parent unless parent.nil?
311
- parent_path = File.dirname(parent_path)
312
- end
313
- return categories.first unless categories.empty?
314
- Page.load('index')
315
- end
316
- end
317
-
318
- def pages
319
- in_category = Page.find_all.select do |page|
320
- page.date.nil? && page.categories.include?(self)
321
- end
322
- in_category.sort do |x, y|
323
- by_priority = y.priority(path) <=> x.priority(path)
324
- if by_priority == 0
325
- x.link_text.downcase <=> y.link_text.downcase
326
- else
327
- by_priority
328
- end
329
- end
330
- end
331
-
332
- def articles
333
- Page.find_articles.select { |article| article.categories.include?(self) }
334
- end
335
-
336
- def receives_comments?
337
- ! date.nil?
338
- end
339
-
340
- private
341
- def category_strings
342
- strings = metadata('categories')
343
- strings.nil? ? [] : strings.split(',').map { |string| string.strip }
344
- end
345
-
346
- def valid_paths(paths)
347
- page_dir = Nesta::Config.page_path
348
- paths.select do |path|
349
- FORMATS.detect do |format|
350
- [path, File.join(path, 'index')].detect do |candidate|
351
- File.exist?(File.join(page_dir, "#{candidate}.#{format}"))
352
- end
353
- end
354
- end
355
- end
356
- end
357
-
358
- class Menu
359
- INDENT = " " * 2
360
-
361
- def self.full_menu
362
- menu = []
363
- menu_file = Nesta::Config.content_path('menu.txt')
364
- if File.exist?(menu_file)
365
- File.open(menu_file) { |file| append_menu_item(menu, file, 0) }
366
- end
367
- menu
368
- end
369
-
370
- def self.top_level
371
- full_menu.reject { |item| item.is_a?(Array) }
372
- end
373
-
374
- def self.for_path(path)
375
- path.sub!(Regexp.new('^/'), '')
376
- if path.empty?
377
- full_menu
378
- else
379
- find_menu_item_by_path(full_menu, path)
380
- end
381
- end
382
-
383
- private_class_method def self.append_menu_item(menu, file, depth)
384
- path = file.readline
385
- rescue EOFError
386
- else
387
- page = Page.load(path.strip)
388
- current_depth = path.scan(INDENT).size
389
- if page
390
- if current_depth > depth
391
- sub_menu_for_depth(menu, depth) << [page]
392
- else
393
- sub_menu_for_depth(menu, current_depth) << page
394
- end
395
- end
396
- append_menu_item(menu, file, current_depth)
397
- end
398
-
399
- private_class_method def self.sub_menu_for_depth(menu, depth)
400
- sub_menu = menu
401
- depth.times { sub_menu = sub_menu[-1] }
402
- sub_menu
403
- end
404
-
405
- private_class_method def self.find_menu_item_by_path(menu, path)
406
- item = menu.detect do |item|
407
- item.respond_to?(:path) && (item.path == path)
408
- end
409
- if item
410
- subsequent = menu[menu.index(item) + 1]
411
- item = [item]
412
- item << subsequent if subsequent.respond_to?(:each)
413
- else
414
- sub_menus = menu.select { |menu_item| menu_item.respond_to?(:each) }
415
- sub_menus.each do |sub_menu|
416
- item = find_menu_item_by_path(sub_menu, path)
417
- break if item
418
- end
419
- end
420
- item
421
- end
422
- end
423
- end
3
+ require_relative './models/file_model'
4
+ require_relative './models/menu'
5
+ require_relative './models/page'
@@ -12,23 +12,9 @@ module Nesta
12
12
  end
13
13
  end
14
14
 
15
- def scss(template, options = {}, locals = {})
16
- find_template(Nesta::App.settings.views, template, Tilt::ScssTemplate) do |file|
17
- return Tilt.new(file).render if File.exist?(file)
18
- end
19
- raise IOError, "SCSS template not found: #{template}"
20
- end
21
-
22
- def sass(template, options = {}, locals = {})
23
- find_template(Nesta::App.settings.views, template, Tilt::SassTemplate) do |file|
24
- return Tilt.new(file).render if File.exist?(file)
25
- end
26
- raise IOError, "Sass template not found: #{template}"
27
- end
28
-
29
15
  def stylesheet(template, options = {}, locals = {})
30
16
  scss(template, options, locals)
31
- rescue IOError
17
+ rescue Errno::ENOENT
32
18
  sass(template, options, locals)
33
19
  end
34
20
  end
data/lib/nesta/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Nesta
2
- VERSION = '0.14.0'
2
+ VERSION = '0.16.0'
3
3
  end
data/nesta.gemspec CHANGED
@@ -36,8 +36,8 @@ EOF
36
36
  s.add_dependency('rdiscount', '~> 2.1')
37
37
  s.add_dependency('RedCloth', '~> 4.2')
38
38
  s.add_dependency('sass-embedded', '~> 1.58')
39
- s.add_dependency('sinatra', '~> 2.0')
40
- s.add_dependency('tilt', '~> 2.0')
39
+ s.add_dependency('sinatra', '~> 3.1')
40
+ s.add_dependency('tilt', '~> 2.1')
41
41
 
42
42
  # Useful in development
43
43
  s.add_development_dependency('mr-sparkle')