nesta 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +13 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +58 -0
- data/LICENSE +19 -0
- data/README.md +45 -0
- data/Rakefile +12 -0
- data/bin/nesta +67 -0
- data/config.ru +9 -0
- data/config/config.yml.sample +73 -0
- data/config/deploy.rb.sample +62 -0
- data/lib/nesta/app.rb +199 -0
- data/lib/nesta/cache.rb +139 -0
- data/lib/nesta/commands.rb +135 -0
- data/lib/nesta/config.rb +87 -0
- data/lib/nesta/models.rb +313 -0
- data/lib/nesta/nesta.rb +0 -0
- data/lib/nesta/overrides.rb +59 -0
- data/lib/nesta/path.rb +11 -0
- data/lib/nesta/plugins.rb +15 -0
- data/lib/nesta/version.rb +3 -0
- data/nesta.gemspec +49 -0
- data/scripts/import-from-mephisto +207 -0
- data/spec/atom_spec.rb +138 -0
- data/spec/commands_spec.rb +220 -0
- data/spec/config_spec.rb +69 -0
- data/spec/model_factory.rb +94 -0
- data/spec/models_spec.rb +445 -0
- data/spec/overrides_spec.rb +113 -0
- data/spec/page_spec.rb +428 -0
- data/spec/path_spec.rb +28 -0
- data/spec/sitemap_spec.rb +102 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +72 -0
- data/templates/Gemfile +8 -0
- data/templates/Rakefile +35 -0
- data/templates/config.ru +9 -0
- data/templates/config/config.yml +73 -0
- data/templates/config/deploy.rb +47 -0
- data/views/analytics.haml +12 -0
- data/views/atom.builder +28 -0
- data/views/categories.haml +3 -0
- data/views/comments.haml +8 -0
- data/views/error.haml +13 -0
- data/views/feed.haml +3 -0
- data/views/index.haml +5 -0
- data/views/layout.haml +27 -0
- data/views/master.sass +246 -0
- data/views/not_found.haml +13 -0
- data/views/page.haml +29 -0
- data/views/sidebar.haml +3 -0
- data/views/sitemap.builder +15 -0
- data/views/summaries.haml +14 -0
- metadata +302 -0
data/lib/nesta/cache.rb
ADDED
@@ -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
|
data/lib/nesta/config.rb
ADDED
@@ -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
|
data/lib/nesta/models.rb
ADDED
@@ -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
|