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/config.rb CHANGED
@@ -1,115 +1,89 @@
1
+ require 'singleton'
1
2
  require 'yaml'
2
3
 
4
+ require_relative './config_file'
5
+
3
6
  module Nesta
4
7
  class Config
8
+ include Singleton
9
+
5
10
  class NotDefined < KeyError; end
6
11
 
7
- @settings = %w[
8
- cache
12
+ SETTINGS = %w[
13
+ author
14
+ build
9
15
  content
10
16
  disqus_short_name
17
+ domain
11
18
  google_analytics_code
12
19
  read_more
13
20
  subtitle
14
21
  theme
15
22
  title
16
23
  ]
17
- @author_settings = %w[name uri email]
18
- @yaml = nil
19
-
24
+
20
25
  class << self
21
- attr_accessor :settings, :author_settings, :yaml_conf
26
+ extend Forwardable
27
+ def_delegators *[:instance, :fetch].concat(SETTINGS.map(&:to_sym))
22
28
  end
23
29
 
24
- def self.fetch(key, *default)
25
- from_environment(key.to_s)
30
+ attr_accessor :config
31
+
32
+ def fetch(setting, *default)
33
+ setting = setting.to_s
34
+ self.config ||= read_config_file(setting)
35
+ env_config = config.fetch(Nesta::App.environment.to_s, {})
36
+ env_config.fetch(
37
+ setting,
38
+ config.fetch(setting) { raise NotDefined.new(setting) }
39
+ )
26
40
  rescue NotDefined
27
- begin
28
- from_yaml(key.to_s)
29
- rescue NotDefined
30
- default.empty? && raise || (return default.first)
31
- end
41
+ default.empty? && raise || (return default.first)
32
42
  end
33
43
 
34
- def self.method_missing(method, *args)
35
- if settings.include?(method.to_s)
36
- fetch(method, nil)
44
+ def method_missing(method, *args)
45
+ if SETTINGS.include?(method.to_s)
46
+ fetch(method.to_s, nil)
37
47
  else
38
48
  super
39
49
  end
40
50
  end
41
-
42
- def self.author
43
- environment_config = {}
44
- %w[name uri email].each do |setting|
45
- variable = "NESTA_AUTHOR__#{setting.upcase}"
46
- ENV[variable] && environment_config[setting] = ENV[variable]
47
- end
48
- environment_config.empty? ? from_yaml('author') : environment_config
49
- rescue NotDefined
50
- nil
51
- end
52
51
 
53
- def self.cache
54
- Nesta.deprecated('Nesta::Config.cache',
55
- 'see http://nestacms.com/docs/deployment/page-caching')
56
- end
57
-
58
- def self.content_path(basename = nil)
59
- get_path(content, basename)
60
- end
61
-
62
- def self.page_path(basename = nil)
63
- get_path(File.join(content_path, "pages"), basename)
52
+ def respond_to_missing?(method, include_private = false)
53
+ SETTINGS.include?(method.to_s) || super
64
54
  end
65
-
66
- def self.attachment_path(basename = nil)
67
- get_path(File.join(content_path, "attachments"), basename)
68
- end
69
-
70
- def self.yaml_path
71
- File.expand_path('config/config.yml', Nesta::App.root)
55
+
56
+ def build
57
+ fetch('build', {})
72
58
  end
73
59
 
74
- def self.read_more
60
+ def read_more
75
61
  fetch('read_more', 'Continue reading')
76
62
  end
77
63
 
78
- def self.from_environment(setting)
79
- value = ENV.fetch("NESTA_#{setting.upcase}")
80
- rescue KeyError
64
+ private
65
+
66
+ def read_config_file(setting)
67
+ YAML::load(ERB.new(IO.read(Nesta::ConfigFile.path)).result)
68
+ rescue Errno::ENOENT
81
69
  raise NotDefined.new(setting)
82
- else
83
- overrides = { "true" => true, "false" => false }
84
- overrides.has_key?(value) ? overrides[value] : value
85
- end
86
- private_class_method :from_environment
87
-
88
- def self.yaml_exists?
89
- File.exist?(yaml_path)
90
70
  end
91
- private_class_method :yaml_exists?
92
71
 
93
- def self.from_hash(hash, setting)
94
- hash.fetch(setting) { raise NotDefined.new(setting) }
95
- end
96
- private_class_method :from_hash
97
-
98
- def self.from_yaml(setting)
99
- raise NotDefined.new(setting) unless yaml_exists?
100
- self.yaml_conf ||= YAML::load(ERB.new(IO.read(yaml_path)).result)
101
- env_config = self.yaml_conf.fetch(Nesta::App.environment.to_s, {})
102
- begin
103
- from_hash(env_config, setting)
104
- rescue NotDefined
105
- from_hash(self.yaml_conf, setting)
106
- end
107
- end
108
- private_class_method :from_yaml
109
-
110
72
  def self.get_path(dirname, basename)
111
73
  basename.nil? ? dirname : File.join(dirname, basename)
112
74
  end
113
75
  private_class_method :get_path
76
+
77
+ def self.content_path(basename = nil)
78
+ get_path(content, basename)
79
+ end
80
+
81
+ def self.page_path(basename = nil)
82
+ get_path(File.join(content_path, "pages"), basename)
83
+ end
84
+
85
+ def self.attachment_path(basename = nil)
86
+ get_path(File.join(content_path, "attachments"), basename)
87
+ end
114
88
  end
115
89
  end
@@ -1,11 +1,15 @@
1
1
  module Nesta
2
2
  class ConfigFile
3
+ def self.path
4
+ File.expand_path('config/config.yml', Nesta::App.root)
5
+ end
6
+
3
7
  def set_value(key, value)
4
8
  pattern = /^\s*#?\s*#{key}:.*/
5
9
  replacement = "#{key}: #{value}"
6
10
 
7
11
  configured = false
8
- File.open(Nesta::Config.yaml_path, 'r+') do |file|
12
+ File.open(self.class.path, 'r+') do |file|
9
13
  output = ''
10
14
  file.each_line do |line|
11
15
  if configured
data/lib/nesta/helpers.rb CHANGED
@@ -60,11 +60,6 @@ module Nesta
60
60
  date.strftime("%d %B %Y")
61
61
  end
62
62
 
63
- def local_stylesheet?
64
- Nesta.deprecated('local_stylesheet?', 'use local_stylesheet_link_tag')
65
- File.exist?(File.expand_path('views/local.sass', Nesta::App.root))
66
- end
67
-
68
63
  def local_stylesheet_link_tag(name)
69
64
  pattern = File.expand_path("views/#{name}.s{a,c}ss", Nesta::App.root)
70
65
  if Dir.glob(pattern).size > 0
@@ -0,0 +1,191 @@
1
+ def register_template_handler(class_name, *extensions)
2
+ Tilt.register Tilt.const_get(class_name), *extensions
3
+ rescue LoadError
4
+ # Only one of the Markdown processors needs to be available, so we can
5
+ # safely ignore these load errors.
6
+ end
7
+
8
+ register_template_handler :MarukuTemplate, 'mdown', 'md'
9
+ register_template_handler :KramdownTemplate, 'mdown', 'md'
10
+ register_template_handler :RDiscountTemplate, 'mdown', 'md'
11
+ register_template_handler :RedcarpetTemplate, 'mdown', 'md'
12
+
13
+
14
+ module Nesta
15
+ class MetadataParseError < RuntimeError; end
16
+
17
+ class FileModel
18
+ FORMATS = [:mdown, :md, :haml, :textile]
19
+ @@model_cache = {}
20
+ @@filename_cache = {}
21
+
22
+ attr_reader :filename, :mtime
23
+
24
+ class CaseInsensitiveHash < Hash
25
+ def [](key)
26
+ super(key.to_s.downcase)
27
+ end
28
+ end
29
+
30
+ def self.model_path(basename = nil)
31
+ Nesta::Config.content_path(basename)
32
+ end
33
+
34
+ def self.find_all
35
+ file_pattern = File.join(model_path, "**", "*.{#{FORMATS.join(',')}}")
36
+ Dir.glob(file_pattern).map do |path|
37
+ relative = path.sub("#{model_path}/", "")
38
+ load(relative.sub(/\.(#{FORMATS.join('|')})/, ""))
39
+ end
40
+ end
41
+
42
+ def self.find_file_for_path(path)
43
+ if ! @@filename_cache.has_key?(path)
44
+ FORMATS.each do |format|
45
+ [path, File.join(path, 'index')].each do |basename|
46
+ filename = model_path("#{basename}.#{format}")
47
+ if File.exist?(filename)
48
+ @@filename_cache[path] = filename
49
+ break
50
+ end
51
+ end
52
+ end
53
+ end
54
+ @@filename_cache[path]
55
+ end
56
+
57
+ def self.needs_loading?(path, filename)
58
+ @@model_cache[path].nil? || File.mtime(filename) > @@model_cache[path].mtime
59
+ end
60
+
61
+ def self.load(path)
62
+ if (filename = find_file_for_path(path)) && needs_loading?(path, filename)
63
+ @@model_cache[path] = self.new(filename)
64
+ end
65
+ @@model_cache[path]
66
+ end
67
+
68
+ def self.purge_cache
69
+ @@model_cache = {}
70
+ @@filename_cache = {}
71
+ end
72
+
73
+ def initialize(filename)
74
+ @filename = filename
75
+ @format = filename.split('.').last.to_sym
76
+ if File.zero?(filename)
77
+ @metadata = {}
78
+ @markup = ''
79
+ else
80
+ @metadata, @markup = parse_file
81
+ end
82
+ @mtime = File.mtime(filename)
83
+ end
84
+
85
+ def ==(other)
86
+ other.respond_to?(:path) && (self.path == other.path)
87
+ end
88
+
89
+ def index_page?
90
+ @filename =~ /\/?index\.\w+$/
91
+ end
92
+
93
+ def abspath
94
+ file_path = @filename.sub(self.class.model_path, '')
95
+ if index_page?
96
+ File.dirname(file_path)
97
+ else
98
+ File.join(File.dirname(file_path), File.basename(file_path, '.*'))
99
+ end
100
+ end
101
+
102
+ def path
103
+ abspath.sub(/^\//, '')
104
+ end
105
+
106
+ def permalink
107
+ File.basename(path)
108
+ end
109
+
110
+ def layout
111
+ (metadata('layout') || 'layout').to_sym
112
+ end
113
+
114
+ def template
115
+ (metadata('template') || 'page').to_sym
116
+ end
117
+
118
+ def to_html(scope = Object.new)
119
+ convert_to_html(@format, scope, markup)
120
+ end
121
+
122
+ def last_modified
123
+ @last_modified ||= File.stat(@filename).mtime
124
+ end
125
+
126
+ def description
127
+ metadata('description')
128
+ end
129
+
130
+ def keywords
131
+ metadata('keywords')
132
+ end
133
+
134
+ def metadata(key)
135
+ @metadata[key]
136
+ end
137
+
138
+ def flagged_as?(flag)
139
+ flags = metadata('flags')
140
+ flags && flags.split(',').map { |name| name.strip }.include?(flag)
141
+ end
142
+
143
+ def parse_metadata(first_paragraph)
144
+ is_metadata = first_paragraph.split("\n").first =~ /^[\w ]+:/
145
+ raise MetadataParseError unless is_metadata
146
+ metadata = CaseInsensitiveHash.new
147
+ first_paragraph.split("\n").each do |line|
148
+ key, value = line.split(/\s*:\s*/, 2)
149
+ next if value.nil?
150
+ metadata[key.downcase] = value.chomp
151
+ end
152
+ metadata
153
+ end
154
+
155
+ private
156
+
157
+ def markup
158
+ @markup
159
+ end
160
+
161
+ def parse_file
162
+ contents = File.open(@filename).read
163
+ rescue Errno::ENOENT
164
+ raise Sinatra::NotFound
165
+ else
166
+ first_paragraph, remaining = contents.split(/\r?\n\r?\n/, 2)
167
+ begin
168
+ return parse_metadata(first_paragraph), remaining
169
+ rescue MetadataParseError
170
+ return {}, contents
171
+ end
172
+ end
173
+
174
+ def add_p_tags_to_haml(text)
175
+ contains_tags = (text =~ /^\s*%/)
176
+ if contains_tags
177
+ text
178
+ else
179
+ text.split(/\r?\n/).inject('') do |accumulator, line|
180
+ accumulator << "%p #{line}\n"
181
+ end
182
+ end
183
+ end
184
+
185
+ def convert_to_html(format, scope, text)
186
+ text = add_p_tags_to_haml(text) if @format == :haml
187
+ template = Tilt[format].new { text }
188
+ template.render(scope)
189
+ end
190
+ end
191
+ end
@@ -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