nesta 0.13.0 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.
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