nesta 0.9.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.
- 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
|