nesta 0.13.0 → 0.15.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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/tests.yml +1 -1
  3. data/.gitignore +1 -0
  4. data/{CHANGES → CHANGELOG.md} +117 -21
  5. data/Gemfile.lock +28 -26
  6. data/LICENSE +1 -1
  7. data/README.md +45 -31
  8. data/RELEASING.md +1 -1
  9. data/bin/nesta +4 -1
  10. data/lib/nesta/app.rb +1 -2
  11. data/lib/nesta/commands/build.rb +38 -0
  12. data/lib/nesta/commands/new.rb +2 -1
  13. data/lib/nesta/commands.rb +1 -0
  14. data/lib/nesta/config.rb +49 -75
  15. data/lib/nesta/config_file.rb +5 -1
  16. data/lib/nesta/helpers.rb +0 -5
  17. data/lib/nesta/models/file_model.rb +191 -0
  18. data/lib/nesta/models/menu.rb +67 -0
  19. data/lib/nesta/models/page.rb +167 -0
  20. data/lib/nesta/models.rb +3 -422
  21. data/lib/nesta/navigation.rb +0 -5
  22. data/lib/nesta/overrides.rb +32 -43
  23. data/lib/nesta/plugin.rb +0 -16
  24. data/lib/nesta/static/assets.rb +50 -0
  25. data/lib/nesta/static/html_file.rb +26 -0
  26. data/lib/nesta/static/site.rb +104 -0
  27. data/lib/nesta/version.rb +1 -1
  28. data/lib/nesta.rb +1 -3
  29. data/nesta.gemspec +5 -5
  30. data/templates/config/config.yml +28 -2
  31. data/templates/themes/README.md +1 -1
  32. data/templates/themes/views/master.sass +1 -1
  33. data/test/integration/atom_feed_test.rb +1 -1
  34. data/test/integration/commands/build_test.rb +53 -0
  35. data/test/integration/overrides_test.rb +1 -1
  36. data/test/integration/sitemap_test.rb +1 -1
  37. data/test/support/temporary_files.rb +1 -1
  38. data/test/support/test_configuration.rb +2 -4
  39. data/test/unit/config_test.rb +25 -94
  40. data/test/unit/static/assets_test.rb +56 -0
  41. data/test/unit/static/html_file_test.rb +41 -0
  42. data/test/unit/static/site_test.rb +104 -0
  43. data/views/atom.haml +2 -2
  44. data/views/comments.haml +2 -2
  45. data/views/footer.haml +1 -1
  46. data/views/header.haml +2 -3
  47. data/views/layout.haml +2 -2
  48. data/views/master.sass +1 -1
  49. data/views/mixins.sass +2 -2
  50. data/views/normalize.scss +0 -1
  51. data/views/sitemap.haml +1 -1
  52. metadata +42 -31
  53. /data/test/unit/{file_model_test.rb → models/file_model_test.rb} +0 -0
  54. /data/test/unit/{menu_test.rb → models/menu_test.rb} +0 -0
  55. /data/test/unit/{page_test.rb → models/page_test.rb} +0 -0
data/lib/nesta/models.rb CHANGED
@@ -1,424 +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
384
- def self.append_menu_item(menu, file, depth)
385
- path = file.readline
386
- rescue EOFError
387
- else
388
- page = Page.load(path.strip)
389
- current_depth = path.scan(INDENT).size
390
- if page
391
- if current_depth > depth
392
- sub_menu_for_depth(menu, depth) << [page]
393
- else
394
- sub_menu_for_depth(menu, current_depth) << page
395
- end
396
- end
397
- append_menu_item(menu, file, current_depth)
398
- end
399
-
400
- def self.sub_menu_for_depth(menu, depth)
401
- sub_menu = menu
402
- depth.times { sub_menu = sub_menu[-1] }
403
- sub_menu
404
- end
405
-
406
- def self.find_menu_item_by_path(menu, path)
407
- item = menu.detect do |item|
408
- item.respond_to?(:path) && (item.path == path)
409
- end
410
- if item
411
- subsequent = menu[menu.index(item) + 1]
412
- item = [item]
413
- item << subsequent if subsequent.respond_to?(:each)
414
- else
415
- sub_menus = menu.select { |menu_item| menu_item.respond_to?(:each) }
416
- sub_menus.each do |sub_menu|
417
- item = find_menu_item_by_path(sub_menu, path)
418
- break if item
419
- end
420
- end
421
- item
422
- end
423
- end
424
- end
3
+ require_relative './models/file_model'
4
+ require_relative './models/menu'
5
+ require_relative './models/page'
@@ -64,11 +64,6 @@ module Nesta
64
64
  raise
65
65
  end
66
66
 
67
- def breadcrumb_label(page)
68
- Nesta.deprecated('breadcrumb_label', 'use link_text')
69
- link_text(page)
70
- end
71
-
72
67
  def current_item?(item)
73
68
  request.path_info == item.abspath
74
69
  end
@@ -1,30 +1,47 @@
1
1
  module Nesta
2
2
  module Overrides
3
3
  module Renderers
4
- def haml(template, options = {}, locals = {})
5
- defaults, engine = Overrides.render_options(template, :haml)
6
- super(template, defaults.merge(options), locals)
7
- end
8
-
9
- def erb(template, options = {}, locals = {})
10
- defaults, engine = Overrides.render_options(template, :erb)
11
- super(template, defaults.merge(options), locals)
4
+ def find_template(views, name, engine, &block)
5
+ user_paths = [
6
+ Nesta::Overrides.local_view_path,
7
+ Nesta::Overrides.theme_view_path,
8
+ views
9
+ ].flatten.compact
10
+ user_paths.each do |path|
11
+ super(path, name, engine, &block)
12
+ end
12
13
  end
13
14
 
14
15
  def scss(template, options = {}, locals = {})
15
- defaults, engine = Overrides.render_options(template, :scss)
16
- super(template, defaults.merge(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}"
17
20
  end
18
21
 
19
22
  def sass(template, options = {}, locals = {})
20
- defaults, engine = Overrides.render_options(template, :sass)
21
- super(template, defaults.merge(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}"
22
27
  end
23
28
 
24
29
  def stylesheet(template, options = {}, locals = {})
25
- defaults, engine = Overrides.render_options(template, :sass, :scss)
26
- renderer = Sinatra::Templates.instance_method(engine)
27
- renderer.bind(self).call(template, defaults.merge(options), locals)
30
+ scss(template, options, locals)
31
+ rescue IOError
32
+ sass(template, options, locals)
33
+ end
34
+ end
35
+
36
+ def self.local_view_path
37
+ Nesta::Path.local("views")
38
+ end
39
+
40
+ def self.theme_view_path
41
+ if Nesta::Config.theme.nil?
42
+ nil
43
+ else
44
+ Nesta::Path.themes(Nesta::Config.theme, "views")
28
45
  end
29
46
  end
30
47
 
@@ -39,33 +56,5 @@ module Nesta
39
56
  require app_file if File.exist?(app_file)
40
57
  end
41
58
  end
42
-
43
- private
44
- def self.template_exists?(engine, views, template)
45
- views && File.exist?(File.join(views, "#{template}.#{engine}"))
46
- end
47
-
48
- def self.render_options(template, *engines)
49
- [local_view_path, theme_view_path].each do |path|
50
- engines.each do |engine|
51
- if template_exists?(engine, path, template)
52
- return { views: path }, engine
53
- end
54
- end
55
- end
56
- [{}, :sass]
57
- end
58
-
59
- def self.local_view_path
60
- Nesta::Path.local("views")
61
- end
62
-
63
- def self.theme_view_path
64
- if Nesta::Config.theme.nil?
65
- nil
66
- else
67
- Nesta::Path.themes(Nesta::Config.theme, "views")
68
- end
69
- end
70
59
  end
71
60
  end
data/lib/nesta/plugin.rb CHANGED
@@ -15,21 +15,5 @@ module Nesta
15
15
  def self.initialize_plugins
16
16
  self.loaded.each { |name| require "#{name}/init" }
17
17
  end
18
-
19
- def self.load_local_plugins
20
- # This approach is deprecated; plugins should now be distributed
21
- # as gems. See http://nestacms.com/docs/plugins/writing-plugins
22
- plugins = Dir.glob(File.expand_path('../plugins/*', File.dirname(__FILE__)))
23
- plugins.each { |path| require_local_plugin(path) }
24
- end
25
-
26
- def self.require_local_plugin(path)
27
- Nesta.deprecated(
28
- 'loading plugins from ./plugins', "convert #{path} to a gem")
29
- require File.join(path, 'lib', File.basename(path))
30
- rescue LoadError => e
31
- $stderr.write("Couldn't load plugins/#{File.basename(path)}: #{e}\n")
32
- end
33
- private_class_method :require_local_plugin
34
18
  end
35
19
  end
@@ -0,0 +1,50 @@
1
+ require 'rake'
2
+
3
+ module Nesta
4
+ module Static
5
+ class Assets
6
+ def initialize(build_dir, logger = nil)
7
+ @build_dir = build_dir
8
+ @logger = logger
9
+ end
10
+
11
+ def copy_attachments
12
+ dest_basename = File.basename(Nesta::Config.attachment_path)
13
+ dest_dir = File.join(@build_dir, dest_basename)
14
+ copy_file_tree(Nesta::Config.attachment_path, dest_dir)
15
+ end
16
+
17
+ def copy_public_folder
18
+ copy_file_tree(Nesta::App.settings.public_folder, @build_dir)
19
+ end
20
+
21
+ private
22
+
23
+ def log(message)
24
+ @logger.call(message) if @logger
25
+ end
26
+
27
+ def copy_file_tree(source_dir, dest_dir)
28
+ files_in_tree(source_dir).each do |file|
29
+ target = File.join(dest_dir, file.sub(/^#{source_dir}\//, ''))
30
+ task = Rake::FileTask.define_task(target => file) do
31
+ target_dir = File.dirname(target)
32
+ FileUtils.mkdir_p(target_dir) unless Dir.exist?(target_dir)
33
+ FileUtils.cp(file, target_dir)
34
+ log("Copied #{file} to #{target}")
35
+ end
36
+ task.invoke
37
+ end
38
+ end
39
+
40
+ def files_in_tree(directory)
41
+ Rake::FileList["#{directory}/**/*"].tap do |assets|
42
+ assets.exclude('~*')
43
+ assets.exclude do |f|
44
+ File.directory?(f)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,26 @@
1
+ module Nesta
2
+ module Static
3
+ class HtmlFile
4
+ def initialize(build_dir, page)
5
+ @build_dir = build_dir
6
+ @content_path = page.filename
7
+ end
8
+
9
+ def page_shares_path_with_directory?(dir, base_without_ext)
10
+ Dir.exist?(File.join(dir, base_without_ext))
11
+ end
12
+
13
+ def filename
14
+ dir, base = File.split(@content_path)
15
+ base_without_ext = File.basename(base, File.extname(base))
16
+ subdir = dir.sub(/^#{Nesta::Config.page_path}/, '')
17
+ path = File.join(@build_dir, subdir, base_without_ext)
18
+ if page_shares_path_with_directory?(dir, base_without_ext)
19
+ File.join(path, 'index.html')
20
+ else
21
+ path + '.html'
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end