ruhoh 2.5 → 2.6

Sign up to get free protection for your applications and to get access to all the features.
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