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.
Files changed (72) hide show
  1. data/Gemfile +1 -1
  2. data/bin/ruhoh +10 -3
  3. data/features/_root.feature +11 -0
  4. data/features/data.feature +78 -0
  5. data/features/javascripts.feature +36 -0
  6. data/features/permalinks.feature +23 -0
  7. data/features/plugins.feature +84 -0
  8. data/features/sort_order.feature +121 -0
  9. data/features/step_defs.rb +3 -3
  10. data/features/support/helpers.rb +3 -5
  11. data/history.json +21 -0
  12. data/lib/ruhoh.rb +28 -123
  13. data/lib/ruhoh/base/collectable.rb +273 -0
  14. data/lib/ruhoh/base/compilable.rb +30 -0
  15. data/lib/ruhoh/base/compilable_asset.rb +30 -0
  16. data/lib/ruhoh/base/model_viewable.rb +30 -0
  17. data/lib/ruhoh/base/modelable.rb +44 -0
  18. data/lib/ruhoh/base/page_like.rb +111 -0
  19. data/lib/ruhoh/base/page_viewable.rb +92 -0
  20. data/lib/ruhoh/base/routable.rb +20 -0
  21. data/lib/ruhoh/base/watchable.rb +18 -0
  22. data/lib/ruhoh/cascade.rb +93 -0
  23. data/lib/ruhoh/client.rb +1 -3
  24. data/lib/ruhoh/collections.rb +2 -1
  25. data/lib/ruhoh/config.rb +67 -0
  26. data/lib/ruhoh/console_methods.rb +0 -2
  27. data/lib/ruhoh/parse.rb +7 -5
  28. data/lib/ruhoh/plugins/initializer.rb +24 -0
  29. data/lib/ruhoh/plugins/local_plugins_plugin.rb +10 -0
  30. data/lib/ruhoh/plugins/plugin.rb +27 -0
  31. data/lib/ruhoh/programs/compile.rb +2 -6
  32. data/lib/ruhoh/programs/preview.rb +5 -2
  33. data/lib/ruhoh/programs/watch.rb +4 -6
  34. data/lib/ruhoh/publish/rsync.rb +2 -2
  35. data/lib/ruhoh/resources/_base/collection.rb +6 -0
  36. data/lib/ruhoh/resources/_base/compiler.rb +3 -0
  37. data/lib/ruhoh/resources/_base/model.rb +3 -0
  38. data/lib/ruhoh/resources/_base/model_view.rb +3 -0
  39. data/lib/ruhoh/resources/_base/watcher.rb +4 -0
  40. data/lib/ruhoh/resources/data/collection.rb +30 -9
  41. data/lib/ruhoh/resources/javascripts/collection_view.rb +5 -1
  42. data/lib/ruhoh/resources/javascripts/model_view.rb +15 -0
  43. data/lib/ruhoh/resources/layouts/client.rb +1 -1
  44. data/lib/ruhoh/resources/pages/client.rb +2 -2
  45. data/lib/ruhoh/resources/pages/collection.rb +2 -21
  46. data/lib/ruhoh/resources/theme/compiler.rb +2 -2
  47. data/lib/ruhoh/resources/widgets/collection.rb +2 -2
  48. data/lib/ruhoh/routes.rb +1 -1
  49. data/lib/ruhoh/summarizer.rb +2 -2
  50. data/lib/ruhoh/ui/dashboard.rb +13 -0
  51. data/lib/ruhoh/ui/page_not_found.rb +3 -2
  52. data/lib/ruhoh/url_slug.rb +23 -9
  53. data/lib/ruhoh/version.rb +1 -1
  54. data/lib/ruhoh/views/master_view.rb +1 -1
  55. data/spec/lib/ruhoh/plugins/initializer_spec.rb +43 -0
  56. data/spec/lib/ruhoh/plugins/plugin_spec.rb +40 -0
  57. data/spec/spec_helper.rb +1 -0
  58. data/system/config.json +21 -0
  59. data/system/{dash/index.html → dashboard.html} +1 -1
  60. data/{lib/ruhoh/ui → system}/page_not_found.html +0 -0
  61. data/system/plugins/sprockets/compiler.rb +1 -0
  62. data/system/widgets/comments/disqus.html +1 -1
  63. metadata +34 -15
  64. data/lib/ruhoh/base/collection.rb +0 -284
  65. data/lib/ruhoh/base/compiler.rb +0 -67
  66. data/lib/ruhoh/base/model.rb +0 -161
  67. data/lib/ruhoh/base/model_view.rb +0 -129
  68. data/lib/ruhoh/base/watcher.rb +0 -25
  69. data/lib/ruhoh/resources/dash/collection.rb +0 -10
  70. data/lib/ruhoh/resources/dash/model.rb +0 -5
  71. data/lib/ruhoh/resources/dash/model_view.rb +0 -5
  72. 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