ruhoh 2.5 → 2.6
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/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
|