ruhoh 2.5 → 2.6
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -1
- data/bin/ruhoh +10 -3
- data/features/_root.feature +11 -0
- data/features/data.feature +78 -0
- data/features/javascripts.feature +36 -0
- data/features/permalinks.feature +23 -0
- data/features/plugins.feature +84 -0
- data/features/sort_order.feature +121 -0
- data/features/step_defs.rb +3 -3
- data/features/support/helpers.rb +3 -5
- data/history.json +21 -0
- data/lib/ruhoh.rb +28 -123
- data/lib/ruhoh/base/collectable.rb +273 -0
- data/lib/ruhoh/base/compilable.rb +30 -0
- data/lib/ruhoh/base/compilable_asset.rb +30 -0
- data/lib/ruhoh/base/model_viewable.rb +30 -0
- data/lib/ruhoh/base/modelable.rb +44 -0
- data/lib/ruhoh/base/page_like.rb +111 -0
- data/lib/ruhoh/base/page_viewable.rb +92 -0
- data/lib/ruhoh/base/routable.rb +20 -0
- data/lib/ruhoh/base/watchable.rb +18 -0
- data/lib/ruhoh/cascade.rb +93 -0
- data/lib/ruhoh/client.rb +1 -3
- data/lib/ruhoh/collections.rb +2 -1
- data/lib/ruhoh/config.rb +67 -0
- data/lib/ruhoh/console_methods.rb +0 -2
- data/lib/ruhoh/parse.rb +7 -5
- data/lib/ruhoh/plugins/initializer.rb +24 -0
- data/lib/ruhoh/plugins/local_plugins_plugin.rb +10 -0
- data/lib/ruhoh/plugins/plugin.rb +27 -0
- data/lib/ruhoh/programs/compile.rb +2 -6
- data/lib/ruhoh/programs/preview.rb +5 -2
- data/lib/ruhoh/programs/watch.rb +4 -6
- data/lib/ruhoh/publish/rsync.rb +2 -2
- data/lib/ruhoh/resources/_base/collection.rb +6 -0
- data/lib/ruhoh/resources/_base/compiler.rb +3 -0
- data/lib/ruhoh/resources/_base/model.rb +3 -0
- data/lib/ruhoh/resources/_base/model_view.rb +3 -0
- data/lib/ruhoh/resources/_base/watcher.rb +4 -0
- data/lib/ruhoh/resources/data/collection.rb +30 -9
- data/lib/ruhoh/resources/javascripts/collection_view.rb +5 -1
- data/lib/ruhoh/resources/javascripts/model_view.rb +15 -0
- data/lib/ruhoh/resources/layouts/client.rb +1 -1
- data/lib/ruhoh/resources/pages/client.rb +2 -2
- data/lib/ruhoh/resources/pages/collection.rb +2 -21
- data/lib/ruhoh/resources/theme/compiler.rb +2 -2
- data/lib/ruhoh/resources/widgets/collection.rb +2 -2
- data/lib/ruhoh/routes.rb +1 -1
- data/lib/ruhoh/summarizer.rb +2 -2
- data/lib/ruhoh/ui/dashboard.rb +13 -0
- data/lib/ruhoh/ui/page_not_found.rb +3 -2
- data/lib/ruhoh/url_slug.rb +23 -9
- data/lib/ruhoh/version.rb +1 -1
- data/lib/ruhoh/views/master_view.rb +1 -1
- data/spec/lib/ruhoh/plugins/initializer_spec.rb +43 -0
- data/spec/lib/ruhoh/plugins/plugin_spec.rb +40 -0
- data/spec/spec_helper.rb +1 -0
- data/system/config.json +21 -0
- data/system/{dash/index.html → dashboard.html} +1 -1
- data/{lib/ruhoh/ui → system}/page_not_found.html +0 -0
- data/system/plugins/sprockets/compiler.rb +1 -0
- data/system/widgets/comments/disqus.html +1 -1
- metadata +34 -15
- data/lib/ruhoh/base/collection.rb +0 -284
- data/lib/ruhoh/base/compiler.rb +0 -67
- data/lib/ruhoh/base/model.rb +0 -161
- data/lib/ruhoh/base/model_view.rb +0 -129
- data/lib/ruhoh/base/watcher.rb +0 -25
- data/lib/ruhoh/resources/dash/collection.rb +0 -10
- data/lib/ruhoh/resources/dash/model.rb +0 -5
- data/lib/ruhoh/resources/dash/model_view.rb +0 -5
- data/lib/ruhoh/resources/dash/previewer.rb +0 -13
@@ -0,0 +1,30 @@
|
|
1
|
+
module Ruhoh::Base::Compilable
|
2
|
+
def self.included(klass)
|
3
|
+
__send__(:attr_reader, :collection)
|
4
|
+
end
|
5
|
+
|
6
|
+
def initialize(collection)
|
7
|
+
@ruhoh = collection.ruhoh
|
8
|
+
@collection = collection
|
9
|
+
end
|
10
|
+
|
11
|
+
def setup_compilable
|
12
|
+
return false unless collection_exists?
|
13
|
+
|
14
|
+
compile_collection_path
|
15
|
+
end
|
16
|
+
|
17
|
+
def compile_collection_path
|
18
|
+
FileUtils.mkdir_p(@collection.compiled_path)
|
19
|
+
end
|
20
|
+
|
21
|
+
def collection_exists?
|
22
|
+
collection = @collection
|
23
|
+
unless @collection.paths?
|
24
|
+
Ruhoh::Friend.say { yellow "#{ collection.resource_name.capitalize }: directory not found - skipping." }
|
25
|
+
return false
|
26
|
+
end
|
27
|
+
Ruhoh::Friend.say { cyan "#{ collection.resource_name.capitalize }: (copying valid files)" }
|
28
|
+
true
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'ruhoh/base/compilable'
|
2
|
+
module Ruhoh::Base::CompilableAsset
|
3
|
+
include Ruhoh::Base::Compilable
|
4
|
+
|
5
|
+
# A basic compiler task which copies each valid collection resource file to the compiled folder.
|
6
|
+
# This is different from the static compiler in that it supports fingerprinting.
|
7
|
+
# Valid files are identified by their pointers.
|
8
|
+
# Invalid files are files that are excluded from the resource's configuration settings.
|
9
|
+
# The collection's url_endpoint is used to determine the final compiled path.
|
10
|
+
#
|
11
|
+
# @returns Nothing.
|
12
|
+
def run
|
13
|
+
return unless setup_compilable
|
14
|
+
|
15
|
+
manifest = {}
|
16
|
+
@collection.files.values.each do |pointer|
|
17
|
+
digest = Digest::MD5.file(pointer['realpath']).hexdigest
|
18
|
+
digest_file = pointer['id'].sub(/\.(\w+)$/) { |ext| "-#{digest}#{ext}" }
|
19
|
+
manifest[pointer['id']] = digest_file
|
20
|
+
|
21
|
+
compiled_file = File.join(@collection.compiled_path, digest_file)
|
22
|
+
FileUtils.mkdir_p File.dirname(compiled_file)
|
23
|
+
FileUtils.cp_r pointer['realpath'], compiled_file
|
24
|
+
Ruhoh::Friend.say { green " > #{pointer['id']}" }
|
25
|
+
end
|
26
|
+
|
27
|
+
# Update the paths to the digest format:
|
28
|
+
@collection.load_collection_view._cache.merge!(manifest)
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Ruhoh::Base::ModelViewable
|
2
|
+
def initialize(model)
|
3
|
+
super(model)
|
4
|
+
@model = model
|
5
|
+
@ruhoh = model.ruhoh
|
6
|
+
|
7
|
+
# Define direct access to the data Hash object
|
8
|
+
# but don't overwrite methods if already defined.
|
9
|
+
data.keys.each do |method|
|
10
|
+
(class << self; self; end).class_eval do
|
11
|
+
next if method_defined?(method)
|
12
|
+
define_method method do |*args, &block|
|
13
|
+
data[method]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def <=>(other)
|
20
|
+
id <=> other.id
|
21
|
+
end
|
22
|
+
|
23
|
+
def [](attribute)
|
24
|
+
__send__(attribute)
|
25
|
+
end
|
26
|
+
|
27
|
+
def []=(key, value)
|
28
|
+
__send__("#{key}=", value)
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Ruhoh::Base::Modelable
|
2
|
+
include Observable
|
3
|
+
|
4
|
+
def self.included(klass)
|
5
|
+
klass.__send__(:attr_reader, :pointer, :ruhoh)
|
6
|
+
end
|
7
|
+
|
8
|
+
def initialize(ruhoh, pointer)
|
9
|
+
raise "Cannot instantiate a model with a nil pointer" unless pointer
|
10
|
+
@ruhoh = ruhoh
|
11
|
+
@pointer = pointer
|
12
|
+
end
|
13
|
+
|
14
|
+
# @returns[Hash Object] Top page metadata
|
15
|
+
def data
|
16
|
+
return @data if @data
|
17
|
+
process
|
18
|
+
@data || {}
|
19
|
+
end
|
20
|
+
|
21
|
+
# @returns[String] Raw (unconverted) page content
|
22
|
+
def content
|
23
|
+
return @content if @content
|
24
|
+
process
|
25
|
+
@content || ''
|
26
|
+
end
|
27
|
+
|
28
|
+
def collection
|
29
|
+
@ruhoh.collection(@pointer['resource'])
|
30
|
+
end
|
31
|
+
|
32
|
+
# Override this to process custom data
|
33
|
+
def process
|
34
|
+
changed
|
35
|
+
notify_observers(@pointer)
|
36
|
+
@pointer
|
37
|
+
end
|
38
|
+
|
39
|
+
def try(method)
|
40
|
+
return __send__(method) if respond_to?(method)
|
41
|
+
return data[method.to_s] if data.key?(method.to_s)
|
42
|
+
false
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'ruhoh/base/modelable'
|
2
|
+
module Ruhoh::Base::PageLike
|
3
|
+
include Ruhoh::Base::Modelable
|
4
|
+
|
5
|
+
DateMatcher = /^(.+\/)*(\d+-\d+-\d+)-(.*)(\.[^.]+)$/
|
6
|
+
Matcher = /^(.+\/)*(.*)(\.[^.]+)$/
|
7
|
+
|
8
|
+
# Process this file. See #parse_page_file
|
9
|
+
# @return[Hash] the processed data from the file.
|
10
|
+
# ex:
|
11
|
+
# { "content" => "..", "data" => { "key" => "value" } }
|
12
|
+
def process
|
13
|
+
return {} unless file?
|
14
|
+
|
15
|
+
parsed_page = parse_page_file
|
16
|
+
data = parsed_page['data']
|
17
|
+
|
18
|
+
filename_data = parse_page_filename(@pointer['id'])
|
19
|
+
|
20
|
+
data['pointer'] = @pointer
|
21
|
+
data['id'] = @pointer['id']
|
22
|
+
|
23
|
+
data['title'] = data['title'] || filename_data['title']
|
24
|
+
data['date'] ||= filename_data['date']
|
25
|
+
|
26
|
+
# Parse and store date as an object
|
27
|
+
begin
|
28
|
+
data['date'] = Time.parse(data['date']) unless data['date'].nil? || data['date'].is_a?(Time)
|
29
|
+
rescue
|
30
|
+
Ruhoh.log.error(
|
31
|
+
"ArgumentError: The date '#{data['date']}' specified in '#{@pointer['id']}' is unparsable."
|
32
|
+
)
|
33
|
+
data['date'] = nil
|
34
|
+
end
|
35
|
+
data['url'] = url(data)
|
36
|
+
data['layout'] = collection.config['layout'] if data['layout'].nil?
|
37
|
+
|
38
|
+
parsed_page['data'] = data
|
39
|
+
|
40
|
+
changed
|
41
|
+
notify_observers(parsed_page)
|
42
|
+
data
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
# Is the resource backed by a physical file in the filesystem?
|
48
|
+
# For example the pagination system uses a page-stub
|
49
|
+
# that has no reference to an actual file.
|
50
|
+
# @return[Boolean]
|
51
|
+
def file?
|
52
|
+
!!@pointer['realpath']
|
53
|
+
end
|
54
|
+
|
55
|
+
# See Ruhoh::Parse.page_file
|
56
|
+
# @returns[Hash Object] processed top meta-data, raw (unconverted) content body
|
57
|
+
def parse_page_file
|
58
|
+
raise "File not found: #{@pointer['realpath']}" unless File.exist?(@pointer['realpath'])
|
59
|
+
result = Ruhoh::Parse.page_file(@pointer['realpath'])
|
60
|
+
|
61
|
+
# variable cache
|
62
|
+
@data = result["data"]
|
63
|
+
@content = result['content']
|
64
|
+
|
65
|
+
result
|
66
|
+
end
|
67
|
+
|
68
|
+
def parse_page_filename(filename)
|
69
|
+
data = *filename.match(DateMatcher)
|
70
|
+
data = *filename.match(Matcher) if data.empty?
|
71
|
+
return {} if data.empty?
|
72
|
+
|
73
|
+
if filename =~ DateMatcher
|
74
|
+
{
|
75
|
+
"path" => data[1],
|
76
|
+
"date" => data[2],
|
77
|
+
"slug" => data[3],
|
78
|
+
"title" => self.to_title(data[3]),
|
79
|
+
"extension" => data[4]
|
80
|
+
}
|
81
|
+
else
|
82
|
+
{
|
83
|
+
"path" => data[1],
|
84
|
+
"slug" => data[2],
|
85
|
+
"title" => to_title(data[2]),
|
86
|
+
"extension" => data[3]
|
87
|
+
}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# my-post-title ===> My Post Title
|
92
|
+
def to_title(file_slug)
|
93
|
+
if file_slug == 'index' && !@pointer['id'].index('/').nil?
|
94
|
+
file_slug = @pointer['id'].split('/')[-2]
|
95
|
+
end
|
96
|
+
|
97
|
+
Ruhoh::StringFormat.titleize(file_slug)
|
98
|
+
end
|
99
|
+
|
100
|
+
def url(page_data)
|
101
|
+
page_data['permalink_ext'] ||= collection.config['permalink_ext']
|
102
|
+
|
103
|
+
format = page_data['permalink'] ||
|
104
|
+
collection.config['permalink'] ||
|
105
|
+
"/:path/:filename"
|
106
|
+
|
107
|
+
slug = Ruhoh::UrlSlug.new(page_data: page_data, format: format)
|
108
|
+
|
109
|
+
@ruhoh.to_url(slug.generate)
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'ruhoh/summarizer'
|
2
|
+
require 'ruhoh/base/model_viewable'
|
3
|
+
module Ruhoh::Base::PageViewable
|
4
|
+
include Ruhoh::Base::ModelViewable
|
5
|
+
|
6
|
+
# Default order by alphabetical title name.
|
7
|
+
def <=>(other)
|
8
|
+
sort = @model.collection.config["sort"] || []
|
9
|
+
attribute = sort[0] || "title"
|
10
|
+
direction = sort[1] || "asc"
|
11
|
+
|
12
|
+
this_data = __send__(attribute)
|
13
|
+
other_data = other.__send__(attribute)
|
14
|
+
if attribute == "date"
|
15
|
+
if this_data.nil? || other_data.nil?
|
16
|
+
Ruhoh.log.error(
|
17
|
+
"ArgumentError:" +
|
18
|
+
" The '#{ @model.collection.resource_name }' collection is configured to sort based on 'date'" +
|
19
|
+
" but '#{ @model.pointer['id'] }' has no parseable date in its metadata." +
|
20
|
+
" Add date: 'YYYY-MM-DD' to its YAML metadata."
|
21
|
+
)
|
22
|
+
end
|
23
|
+
direction = sort[1] || "desc" #default should be reverse
|
24
|
+
end
|
25
|
+
|
26
|
+
if direction == "asc"
|
27
|
+
this_data <=> other_data
|
28
|
+
else
|
29
|
+
other_data <=> this_data
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def categories
|
34
|
+
@model.collection.to_categories(data['categories'])
|
35
|
+
end
|
36
|
+
|
37
|
+
def tags
|
38
|
+
@model.collection.to_tags(data['tags'])
|
39
|
+
end
|
40
|
+
|
41
|
+
# Lazy-load the page body.
|
42
|
+
# Notes:
|
43
|
+
# @content is not used for caching, it's used to manually
|
44
|
+
# define content for a given page. Useful in the case that
|
45
|
+
# you want to model a resource that does not actually
|
46
|
+
# reference a file.
|
47
|
+
def content
|
48
|
+
return @content if @content
|
49
|
+
content = @model.collection.master.render(@model.content)
|
50
|
+
Ruhoh::Converter.convert(content, id)
|
51
|
+
end
|
52
|
+
|
53
|
+
def is_active_page
|
54
|
+
id == @model.collection.master.page_data['id']
|
55
|
+
end
|
56
|
+
|
57
|
+
def summary
|
58
|
+
model_data = @model.data
|
59
|
+
collection_config = @model.collection.config
|
60
|
+
|
61
|
+
line_limit = model_data['summary_lines'] ||
|
62
|
+
collection_config['summary_lines']
|
63
|
+
stop_at_header = model_data['summary_stop_at_header'] ||
|
64
|
+
collection_config['summary_stop_at_header']
|
65
|
+
|
66
|
+
Ruhoh::Summarizer.new({
|
67
|
+
content: @ruhoh.master_view(@model.pointer).render_content,
|
68
|
+
line_limit: line_limit,
|
69
|
+
stop_at_header: stop_at_header
|
70
|
+
}).generate
|
71
|
+
end
|
72
|
+
|
73
|
+
def next
|
74
|
+
return unless id
|
75
|
+
all_cache = @model.collection.all
|
76
|
+
index = all_cache.index {|p| p["id"] == id}
|
77
|
+
return unless index && (index-1 >= 0)
|
78
|
+
_next = all_cache[index-1]
|
79
|
+
return unless _next
|
80
|
+
_next
|
81
|
+
end
|
82
|
+
|
83
|
+
def previous
|
84
|
+
return unless id
|
85
|
+
all_cache = @model.collection.all
|
86
|
+
index = all_cache.index {|p| p["id"] == id}
|
87
|
+
return unless index && (index+1 >= 0)
|
88
|
+
prev = all_cache[index+1]
|
89
|
+
return unless prev
|
90
|
+
prev
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# Makes a collection routable.
|
2
|
+
module Ruhoh::Base::Routable
|
3
|
+
def routes
|
4
|
+
return @routes if @routes
|
5
|
+
@routes = {}
|
6
|
+
dictionary
|
7
|
+
@routes
|
8
|
+
end
|
9
|
+
|
10
|
+
def routes_add(route, pointer)
|
11
|
+
@routes ||= {}
|
12
|
+
@routes[route] = pointer
|
13
|
+
end
|
14
|
+
|
15
|
+
def routes_delete(pointer)
|
16
|
+
return unless @routes
|
17
|
+
route = @routes.find{ |k, v| v == pointer }
|
18
|
+
@routes.delete(route[0]) if route
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Ruhoh::Base::Watchable
|
2
|
+
def self.included(klass)
|
3
|
+
klass.__send__(:attr_accessor, :collection)
|
4
|
+
end
|
5
|
+
|
6
|
+
def initialize(collection)
|
7
|
+
@collection = collection
|
8
|
+
end
|
9
|
+
|
10
|
+
def update(path)
|
11
|
+
# Drop the resource namespace
|
12
|
+
matcher = File::ALT_SEPARATOR ?
|
13
|
+
%r{^.+(#{ File::SEPARATOR }|#{ File::ALT_SEPARATOR })} :
|
14
|
+
%r{^.+#{ File::SEPARATOR }}
|
15
|
+
|
16
|
+
collection.touch(path.gsub(matcher, ''))
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
class Ruhoh
|
2
|
+
class Cascade
|
3
|
+
|
4
|
+
attr_reader :config
|
5
|
+
attr_accessor :theme, :base, :system
|
6
|
+
|
7
|
+
def initialize(config)
|
8
|
+
@config = config
|
9
|
+
config.add_observer(self)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Find a file in the base cascade directories
|
13
|
+
# @return[Hash, nil] a single file pointer
|
14
|
+
def find_file(key)
|
15
|
+
dict = _all_files
|
16
|
+
dict[key] || dict.values.find{ |a| key == a['id'].gsub(/.[^.]+$/, '') }
|
17
|
+
end
|
18
|
+
|
19
|
+
def merge_data_file(key)
|
20
|
+
realpaths = []
|
21
|
+
paths.map{ |a| a['path'] }.each do |path|
|
22
|
+
FileUtils.cd(path) {
|
23
|
+
match = Dir["*"].find { |id|
|
24
|
+
File.exist?(id) &&
|
25
|
+
FileTest.file?(id) &&
|
26
|
+
id.gsub(/.[^.]+$/, '') == key
|
27
|
+
}
|
28
|
+
next unless match
|
29
|
+
realpaths << File.realpath(match)
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
return nil unless realpaths && !realpaths.empty?
|
34
|
+
|
35
|
+
data = {}
|
36
|
+
realpaths.each do |path|
|
37
|
+
data = Ruhoh::Utils.deep_merge(data, (Ruhoh::Parse.data_file(path) || {}))
|
38
|
+
end
|
39
|
+
|
40
|
+
data
|
41
|
+
end
|
42
|
+
|
43
|
+
# Collect all files from the base cascade directories.
|
44
|
+
# @return[Hash] dictionary of file pointers
|
45
|
+
def _all_files
|
46
|
+
dict = {}
|
47
|
+
paths.map{ |a| a['path'] }.each do |path|
|
48
|
+
FileUtils.cd(path) {
|
49
|
+
Dir["*"].each { |id|
|
50
|
+
next unless File.exist?(id) && FileTest.file?(id)
|
51
|
+
dict[id] = {
|
52
|
+
"id" => id,
|
53
|
+
"realpath" => File.realpath(id),
|
54
|
+
}
|
55
|
+
}
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
dict
|
60
|
+
end
|
61
|
+
|
62
|
+
# When config is updated
|
63
|
+
def update(config_data)
|
64
|
+
if config_data['_theme_collection']
|
65
|
+
@theme = File.join(base, config_data['_theme_collection'])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Default paths to the 3 levels of the cascade.
|
70
|
+
def paths
|
71
|
+
a = [
|
72
|
+
{
|
73
|
+
"name" => "system",
|
74
|
+
"path" => system
|
75
|
+
},
|
76
|
+
{
|
77
|
+
"name" => "base",
|
78
|
+
"path" => base
|
79
|
+
}
|
80
|
+
]
|
81
|
+
a << {
|
82
|
+
"name" => "theme",
|
83
|
+
"path" => theme
|
84
|
+
} if theme
|
85
|
+
|
86
|
+
a
|
87
|
+
end
|
88
|
+
|
89
|
+
def system
|
90
|
+
File.join(Ruhoh::Root, "system")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|