ruhoh 2.2 → 2.3

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -3,7 +3,7 @@ gemspec
3
3
 
4
4
  gem 'rack', "~> 1.4"
5
5
  gem 'directory_watcher', "~> 1.4.0"
6
- gem 'psych', "~> 1.3", :platforms => [:ruby_18, :mingw_18]
6
+ #gem 'psych', "~> 1.3", :platforms => [:ruby_18, :mingw_18]
7
7
  gem 'redcarpet', "~> 2.1"
8
8
  gem 'nokogiri', "~> 1.5"
9
9
 
data/README.md CHANGED
@@ -1,3 +1,4 @@
1
+ [![Stories in Ready](https://badge.waffle.io/ruhoh/ruhoh.rb.png?label=ready)](https://waffle.io/ruhoh/ruhoh.rb)
1
2
  [![Build Status](https://travis-ci.org/ruhoh/ruhoh.rb.png?branch=master)](https://travis-ci.org/ruhoh/ruhoh.rb)
2
3
 
3
4
  ## Ruhoh is the Universal Static Blog API
@@ -0,0 +1,22 @@
1
+ Feature: Config
2
+ As a content publisher
3
+ I want to configure the way my site works
4
+ so I can publish content in a way that makes me happy.
5
+
6
+ Scenario: Setting a production_url
7
+ Given some files with values:
8
+ | file | body |
9
+ | config.yml | production_url: 'http://hello-world.com' |
10
+ | _root/index.html | <span>{{ urls.production }}</span> |
11
+ When I compile my site
12
+ Then my compiled site should have the file "index.html"
13
+ And this file should contain the content node "span|http://hello-world.com"
14
+
15
+ Scenario: Setting a production_url and using legacy urls.production_url
16
+ Given some files with values:
17
+ | file | body |
18
+ | config.yml | production_url: 'http://hello-world.com' |
19
+ | _root/index.html | <span>{{ urls.production_url }}</span> |
20
+ When I compile my site
21
+ Then my compiled site should have the file "index.html"
22
+ And this file should contain the content node "span|http://hello-world.com"
@@ -0,0 +1,14 @@
1
+ Feature: Ignored resources
2
+ As a content publisher
3
+ I want to ignore certain folders
4
+ so that I can manage non-website related resources freely without screwing up my website.
5
+
6
+ Scenario: Defining an ignored resource (directory)
7
+ Given a config file with values:
8
+ | deploy | { "use" : "ignore" } |
9
+ Given some files with values:
10
+ | file |
11
+ | deploy/hello.md |
12
+ When I compile my site
13
+ Then my compiled site should NOT have the file "deploy/hello/index.html"
14
+ Then my compiled site should NOT have the folder "deploy"
@@ -54,6 +54,29 @@ Feature: Page Permalinks
54
54
  When I compile my site
55
55
  Then my compiled site should have the file "essays/2012/1/2/hello/index.html"
56
56
 
57
+ Scenario: Custom permalink format in page metadata using explicit html extension
58
+ Given some files with values:
59
+ | file | permalink |
60
+ | essays/hello.md | :filename.html |
61
+ When I compile my site
62
+ Then my compiled site should have the file "hello.html"
63
+
64
+ Scenario: Custom permalink format in page metadata using explicit convertable extension
65
+ Given some files with values:
66
+ | file | permalink |
67
+ | essays/hello.md | :filename.md |
68
+ When I compile my site
69
+ Then my compiled site should have the file "hello.md"
70
+
71
+ Scenario: Custom permalink format in page metadata using explicit arbitrary extension
72
+ Given some files with values:
73
+ | file | permalink |
74
+ | essays/hello.md | :filename.derp |
75
+ When I compile my site
76
+ Then my compiled site should have the file "hello.derp"
77
+
78
+ ## Literal permalink
79
+
57
80
  Scenario: Literal permalink format in page metadata.
58
81
  Given some files with values:
59
82
  | file | permalink | body |
@@ -0,0 +1,19 @@
1
+ Feature: Static resources
2
+ As a content publisher
3
+ I want to include static folders
4
+ so that I can statically transfer files over to my website output.
5
+
6
+ Scenario: Defining an ignored resource (directory)
7
+ Given a config file with values:
8
+ | recipes | { "use" : "static" } |
9
+ Given some files with values:
10
+ | file |
11
+ | recipes/hello.md |
12
+ | recipes/hi.txt |
13
+ | recipes/yo.html |
14
+ | recipes/cool/data.json |
15
+ When I compile my site
16
+ Then my compiled site should have the file "recipes/hello.md"
17
+ Then my compiled site should have the file "recipes/hi.txt"
18
+ Then my compiled site should have the file "recipes/yo.html"
19
+ Then my compiled site should have the file "recipes/cool/data.json"
@@ -34,6 +34,15 @@ Then(/^my compiled site (should|should NOT) have the file "(.*?)"$/) do |matcher
34
34
  }
35
35
  end
36
36
 
37
+ Then(/^my compiled site (should|should NOT) have the (?:directory|folder) "(.*?)"$/) do |matcher, path|
38
+ @filepath = path
39
+ FileUtils.cd(@ruhoh.paths.compiled) {
40
+ # Done this way so the error output is more informative.
41
+ files = Dir.glob("**/*").delete_if{ |a| File.file?(a) }
42
+ files.__send__(matcher, include(path))
43
+ }
44
+ end
45
+
37
46
  Then(/^this file (should|should NOT) (?:have|contain) the content "(.*?)"$/) do |matcher, content|
38
47
  this_compiled_file.__send__(matcher, have_content(content))
39
48
  end
@@ -28,6 +28,30 @@ Feature: Summary
28
28
  And this file should contain the content node "div.summary|The Giraffe and the Zebra and the Eland and the Koodoo and the Hartebeest lived there: and they were 'sclusively sandy-yellow-brownish all over; but the Leopard, he was the 'sclusivest sandiest-yellowest-brownest of them all -- a greyish-yellowish catty-shaped kind of beast, and he matched the 'sclusively yellowish-greyish-brownish colour of the High Veldt to one hair."
29
29
  And this file should NOT contain the content "This was very bad for the Giraffe and the Zebra and the rest of them: for he would lie down by a 'sclusively yellowish-greyish-brownish stone or clump of grass, and when the Giraffe or the Zebra or the Eland or the Koodoo or the Bush-Buck or the Bonte-Buck came by he would surprise them out of their jumpsome lives."
30
30
 
31
+ Scenario: Summary with configured summary_lines in page's meta-data
32
+ Given some files with values:
33
+ | file | body |
34
+ | layouts/essays.md | {{{ page.summary }}} |
35
+ And the file "essays/hello.md" with body:
36
+ """
37
+ ---
38
+ summary_lines: 2
39
+ ---
40
+ In the days when everybody started fair, Best Beloved, the Leopard lived in a place called the High Veldt. 'Member it wasn't the Low Veldt, or the Bush Veldt, or the Sour Veldt, but the 'sclusively bare, hot shiny High Veldt, where there was sand and sandy-coloured rock and 'sclusively tufts of sandy-yellowish grass.
41
+
42
+ The Giraffe and the Zebra and the Eland and the Koodoo and the Hartebeest lived there: and they were 'sclusively sandy-yellow-brownish all over; but the Leopard, he was the 'sclusivest sandiest-yellowest-brownest of them all -- a greyish-yellowish catty-shaped kind of beast, and he matched the 'sclusively yellowish-greyish-brownish colour of the High Veldt to one hair.
43
+
44
+ This was very bad for the Giraffe and the Zebra and the rest of them: for he would lie down by a 'sclusively yellowish-greyish-brownish stone or clump of grass, and when the Giraffe or the Zebra or the Eland or the Koodoo or the Bush-Buck or the Bonte-Buck came by he would surprise them out of their jumpsome lives.
45
+
46
+ He would indeed! And, also, there was an Ethiopian with bows and arrows (a 'sclusively greyish-brownish-yellowish man he was then), who lived on the High Veldt with the Leopard: and the two used to hunt together -- the Ethiopian with his bows and arrows, and the Leopard 'sclusively with his teeth and claws -- till the Giraffe and the Eland and the Koodoo and the Quagga and all the rest of them didn't know which way to jump, Best Beloved.
47
+ They didn't indeed!
48
+ """
49
+ When I compile my site
50
+ Then my compiled site should have the file "essays/hello/index.html"
51
+ And this file should contain the content node "div.summary|In the days when everybody started fair, Best Beloved, the Leopard lived in a place called the High Veldt. 'Member it wasn't the Low Veldt, or the Bush Veldt, or the Sour Veldt, but the 'sclusively bare, hot shiny High Veldt, where there was sand and sandy-coloured rock and 'sclusively tufts of sandy-yellowish grass."
52
+ And this file should contain the content node "div.summary|The Giraffe and the Zebra and the Eland and the Koodoo and the Hartebeest lived there: and they were 'sclusively sandy-yellow-brownish all over; but the Leopard, he was the 'sclusivest sandiest-yellowest-brownest of them all -- a greyish-yellowish catty-shaped kind of beast, and he matched the 'sclusively yellowish-greyish-brownish colour of the High Veldt to one hair."
53
+ And this file should NOT contain the content "This was very bad for the Giraffe and the Zebra and the rest of them: for he would lie down by a 'sclusively yellowish-greyish-brownish stone or clump of grass, and when the Giraffe or the Zebra or the Eland or the Koodoo or the Bush-Buck or the Bonte-Buck came by he would surprise them out of their jumpsome lives."
54
+
31
55
  Scenario: Specifying an explicit summary DOM node
32
56
  Given some files with values:
33
57
  | file | body |
data/history.json CHANGED
@@ -1,10 +1,29 @@
1
1
  [
2
+ {
3
+ "version" : "2.3",
4
+ "date" : "25.08.2013",
5
+ "changes" : [
6
+ "(Internal) Ruhoh::CleanUrl and Ruhoh::StringFormat better encapsulate url-slug generation.",
7
+ "(Internal) page.summary is now single-purpose Ruhoh::Summarizer service."
8
+ ],
9
+ "features" : [
10
+ "Add ability to use: 'ignore' to ignore a collection folder.",
11
+ "Add ability to use: 'static' to serve a collection folder staticly.",
12
+ "Add title into base scaffold if defined from CLI."
13
+ ],
14
+ "bugs" : [
15
+ "Fix plugins not running on the CLI.",
16
+ "Fix permalinks not persisting explicit extentions, e.g. .html",
17
+ "Expose urls.production and urls.production_url in the view."
18
+ ]
19
+ }
20
+ ,
2
21
  {
3
22
  "version" : "2.2",
4
23
  "date" : "09.06.2013",
5
24
  "changes" : [
6
25
  "@binaryphile updates google analytics tracking script",
7
- "@stebalien overhauls page.summary internal implementation",
26
+ "@stebalien overhauls page.summary internal implementation"
8
27
  ],
9
28
  "features" : [
10
29
  "Cache the collection#all call to improve performance.",
@@ -19,7 +38,7 @@
19
38
  "@caspervonb makes widgets compiler respect model excludes",
20
39
  "Avoid data parse errors by always converting to string",
21
40
  "Improve error messages for a few parse error cases",
22
- "Fix widget config not deep merging with default widget config",
41
+ "Fix widget config not deep merging with default widget config"
23
42
  ]
24
43
  },
25
44
  {
@@ -15,6 +15,7 @@ module Ruhoh::Base
15
15
  include Compilable
16
16
 
17
17
  # A basic compiler task which copies each valid collection resource file to the compiled folder.
18
+ # This is different from the static compiler in that it supports fingerprinting.
18
19
  # Valid files are identified by their pointers.
19
20
  # Invalid files are files that are excluded from the resource's configuration settings.
20
21
  # The collection's url_endpoint is used to determine the final compiled path.
@@ -39,7 +39,7 @@ module Ruhoh::Base
39
39
 
40
40
  def try(method)
41
41
  return __send__(method) if respond_to?(method)
42
- return data[method] if data.key?(method.to_s)
42
+ return data[method.to_s] if data.key?(method.to_s)
43
43
  false
44
44
  end
45
45
  end
@@ -68,7 +68,7 @@ module Ruhoh::Base
68
68
 
69
69
  data['title'] = data['title'] || filename_data['title']
70
70
  data['date'] ||= filename_data['date'].to_s
71
- data['url'] = permalink(data)
71
+ data['url'] = url(data)
72
72
  data['layout'] = collection.config['layout'] if data['layout'].nil?
73
73
 
74
74
  parsed_page['data'] = data
@@ -157,80 +157,19 @@ module Ruhoh::Base
157
157
  file_slug = @pointer['id'].split('/')[-2]
158
158
  end
159
159
 
160
- file_slug.gsub(/[^\p{Word}+]/u, ' ').gsub(/\b\w/){$&.upcase}
160
+ Ruhoh::StringFormat.titleize(file_slug)
161
161
  end
162
162
 
163
- # Another blatently stolen method from Jekyll
164
- # The category is only the first one if multiple categories exist.
165
- def permalink(page_data)
166
- format = page_data['permalink'] || collection.config['permalink']
167
- format ||= "/:path/:filename"
168
-
169
- url = if format.include?(':')
170
- title = Ruhoh::Utils.to_url_slug(page_data['title'])
171
- filename = File.basename(page_data['id'])
172
- category = Array(page_data['categories'])[0]
173
- category = category.split('/').map {|c| Ruhoh::Utils.to_url_slug(c) }.join('/') if category
174
- relative_path = File.dirname(page_data['id'])
175
- relative_path = "" if relative_path == "."
176
- data = {
177
- "title" => title,
178
- "filename" => filename,
179
- "path" => File.join(@pointer["resource"], relative_path),
180
- "relative_path" => relative_path,
181
- "categories" => category || '',
182
- }
183
-
184
- uses_date = false
185
- %w{ :year :month :day :i_day :i_month }.each do |token|
186
- if format.include?(token)
187
- uses_date = true
188
- break
189
- end
190
- end
191
-
192
- if uses_date
193
- begin
194
- date = Time.parse(page_data['date'].to_s)
195
- rescue ArgumentError, TypeError
196
- Ruhoh.log.error(
197
- "ArgumentError:" +
198
- " The file '#{ @pointer["realpath"] }' has a permalink '#{ format }'" +
199
- " which is date dependant but the date '#{page_data['date']}' could not be parsed." +
200
- " Ensure the date's format is: 'YYYY-MM-DD'"
201
- )
202
- end
203
-
204
- data.merge!({
205
- "year" => date.strftime("%Y"),
206
- "month" => date.strftime("%m"),
207
- "day" => date.strftime("%d"),
208
- "i_day" => date.strftime("%d").to_i.to_s,
209
- "i_month" => date.strftime("%m").to_i.to_s,
210
- })
211
- end
212
-
213
- data.inject(format) { |result, token|
214
- result.gsub(/:#{Regexp.escape token.first}/, token.last)
215
- }.gsub(/\/+/, "/")
216
- else
217
- # Use the literal permalink if it is a non-tokenized string.
218
- format.gsub(/^\//, '').split('/').map {|p| CGI::escape(p) }.join('/')
219
- end
163
+ def url(page_data)
164
+ page_data['permalink_ext'] ||= collection.config['permalink_ext']
220
165
 
221
- # Only recognize extensions registered from a 'convertable' module.
222
- # This means 'non-convertable' extensions should pass-through.
223
- if Ruhoh::Converter.extensions.include?(File.extname(url))
224
- url = url.gsub(%r{#{File.extname(url)}$}, '.html')
225
- end
226
-
227
- unless (page_data['permalink_ext'] || collection.config['permalink_ext'])
228
- url = url.gsub(/index.html$/, '').gsub(/\.html$/, '')
229
- end
166
+ format = page_data['permalink'] ||
167
+ collection.config['permalink'] ||
168
+ "/:path/:filename"
230
169
 
231
- url = '/' if url.empty?
170
+ slug = Ruhoh::UrlSlug.new(page_data: page_data, format: format)
232
171
 
233
- @ruhoh.to_url(url)
172
+ @ruhoh.to_url(slug.generate)
234
173
  end
235
174
  end
236
175
 
@@ -1,5 +1,5 @@
1
- require 'nokogiri'
2
1
  require 'set'
2
+ require 'ruhoh/summarizer'
3
3
 
4
4
  module Ruhoh::Base
5
5
  module ModelViewable
@@ -89,72 +89,21 @@ module Ruhoh::Base
89
89
  def is_active_page
90
90
  id == @model.collection.master.page_data['id']
91
91
  end
92
-
93
- # Generate a truncated summary.
94
- # - If a summary element (`<tag class="summary">...</tag>`) is specified
95
- # in the content, return it.
96
- # - If summary_lines > 0, truncate after the first complete element where
97
- # the number of summary lines is greater than summary_lines.
98
- # - If summary_stop_at_header is a number n, stop before the nth header.
99
- # - If summary_stop_at_header is true, stop before the first header after
100
- # content has been included. In other words, don't count headers at the
101
- # top of the page.
102
- def summary
103
- # Parse the document
104
- full_content = @ruhoh.master_view(@model.pointer).render_content
105
- content_doc = Nokogiri::HTML.fragment(full_content)
106
-
107
- # Return a summary element if specified
108
- summary_el = content_doc.at_css('.summary')
109
- return summary_el.to_html unless summary_el.nil?
110
92
 
111
- # Get the configuration parameters
112
- # Default to the parameters provided in the page itself
93
+ def summary
113
94
  model_data = @model.data
114
95
  collection_config = @model.collection.config
115
- line_limit = model_data['summary_lines'] || collection_config['summary_lines']
116
- stop_at_header = model_data['summary_stop_at_header']
117
- stop_at_header = collection_config['summary_stop_at_header'] if stop_at_header.nil?
118
-
119
- # Create the summary element.
120
- summary_doc = Nokogiri::XML::Node.new("div", Nokogiri::HTML::Document.new)
121
- summary_doc["class"] = "summary"
122
96
 
123
- # All "heading" elements.
124
- headings = Nokogiri::HTML::ElementDescription::HEADING + ["header", "hgroup"]
125
-
126
-
127
- content_doc.children.each do |node|
128
-
129
- if stop_at_header == true
130
- # Detect first header after content
131
- if not (headings.include?(node.name) && node.content.empty?)
132
- stop_at_header = 1
133
- end
134
- elsif stop_at_header.is_a?(Integer) && headings.include?(node.name)
135
- if stop_at_header > 1
136
- stop_at_header -= 1;
137
- else
138
- summary_doc["class"] += " ellipsis"
139
- break
140
- end
141
- end
142
-
143
- if line_limit > 0 && summary_doc.content.lines.to_a.length > line_limit
144
- # Skip through leftover whitespace. Without this check, the summary
145
- # can be marked as ellipsis even if it isn't.
146
- unless node.text? && node.text.strip.empty?
147
- summary_doc["class"] += " ellipsis"
148
- break
149
- else
150
- next
151
- end
152
- end
153
-
154
- summary_doc << node
155
- end
97
+ line_limit = model_data['summary_lines'] ||
98
+ collection_config['summary_lines']
99
+ stop_at_header = model_data['summary_stop_at_header'] ||
100
+ collection_config['summary_stop_at_header']
156
101
 
157
- summary_doc.to_html
102
+ Ruhoh::Summarizer.new({
103
+ content: @ruhoh.master_view(@model.pointer).render_content,
104
+ line_limit: line_limit,
105
+ stop_at_header: stop_at_header
106
+ }).generate
158
107
  end
159
108
 
160
109
  def next
data/lib/ruhoh/client.rb CHANGED
@@ -30,6 +30,7 @@ class Ruhoh
30
30
 
31
31
  @ruhoh.setup
32
32
  @ruhoh.setup_paths
33
+ @ruhoh.setup_plugins
33
34
 
34
35
  return __send__(cmd) if respond_to?(cmd)
35
36
 
@@ -113,8 +113,6 @@ class Ruhoh
113
113
 
114
114
  def url_endpoints
115
115
  urls = {}
116
- urls["base_path"] = @ruhoh.base_path
117
-
118
116
  all.each do |name|
119
117
  collection = load(name)
120
118
  next unless collection.respond_to?(:url_endpoint)
@@ -169,4 +167,4 @@ class Ruhoh
169
167
  name.to_s.split('_').map {|a| a.capitalize}.join
170
168
  end
171
169
  end
172
- end
170
+ end
@@ -36,8 +36,8 @@ class Ruhoh
36
36
  # since presumably they define customized permalinks per singular resource.
37
37
  # Page-like resources are handled the root mapping below.
38
38
  ruhoh.collections.url_endpoints_sorted.each do |h|
39
- # Omit base_path and theme because they are special use-cases.
40
- next if %w{base_path theme}.include?(h["name"])
39
+ # Omit theme because they are special use-cases.
40
+ next if %w{ theme }.include?(h["name"])
41
41
  map h["url"] do
42
42
  collection = ruhoh.collection(h["name"])
43
43
  if collection.previewer?
@@ -27,15 +27,22 @@ class Ruhoh
27
27
  yellow "Watch [#{Time.now.strftime("%H:%M:%S")}] [Update #{path}] : #{args.size} files changed"
28
28
  }
29
29
 
30
- separator = File::ALT_SEPARATOR ?
31
- %r{#{ File::SEPARATOR }|#{ File::ALT_SEPARATOR }} :
32
- File::SEPARATOR
33
- resource = path.split(separator)[0]
34
-
35
- ruhoh.cache.delete(ruhoh.collection(resource).files_cache_key)
36
- ruhoh.cache.delete("#{ resource }-all")
37
-
38
- ruhoh.collection(resource).load_watcher.update(path)
30
+ if path == "config.yml"
31
+ ruhoh.config true
32
+ else
33
+ separator = File::ALT_SEPARATOR ?
34
+ %r{#{ File::SEPARATOR }|#{ File::ALT_SEPARATOR }} :
35
+ File::SEPARATOR
36
+ resource = path.split(separator)[0]
37
+
38
+ ruhoh.cache.delete(ruhoh.collection(resource).files_cache_key)
39
+ ruhoh.cache.delete("#{ resource }-all")
40
+
41
+ puts("HERE", resource)
42
+
43
+ ruhoh.collection(resource).load_watcher.update(path)
44
+ puts(ruhoh.collection(resource))
45
+ end
39
46
  end
40
47
  end
41
48
 
@@ -43,4 +50,4 @@ class Ruhoh
43
50
  end
44
51
 
45
52
  end
46
- end
53
+ end
@@ -0,0 +1,5 @@
1
+ module Ruhoh::Resources::Ignore
2
+ class Collection
3
+ include Ruhoh::Base::Collectable
4
+ end
5
+ end
@@ -43,7 +43,7 @@ module Ruhoh::Resources::Pages
43
43
  def titleize
44
44
  @collection.dictionary.each do |id, data|
45
45
  next unless File.basename(data['id']) =~ /^untitled/
46
- new_name = Ruhoh::Utils.to_slug(data['title'])
46
+ new_name = Ruhoh::StringFormat.clean_slug(data['title'])
47
47
  new_file = "#{new_name}#{File.extname(data['id'])}"
48
48
  old_file = File.basename(data['id'])
49
49
  next if old_file == new_file
@@ -63,13 +63,35 @@ module Ruhoh::Resources::Pages
63
63
  _list(@collection.all)
64
64
  end
65
65
 
66
+ # Public - renders the scaffold, if available, for this resource.
67
+ def render_scaffold
68
+ (@collection.scaffold || '')
69
+ .gsub('{{DATE}}', Time.now.strftime('%Y-%m-%d'))
70
+ .gsub('{{TITLE}}', @args[2])
71
+ end
72
+
66
73
  protected
67
74
 
68
75
  def create(opts={})
76
+ filename = ensure_unique_filename(@args[2], opts)
77
+ FileUtils.mkdir_p File.dirname(filename)
78
+
79
+ File.open(filename, 'w:UTF-8') { |f| f.puts render_scaffold }
80
+
69
81
  ruhoh = @ruhoh
82
+ resource_name = @collection.resource_name
83
+ Ruhoh::Friend.say {
84
+ green "New #{resource_name}:"
85
+ green " > #{ruhoh.relative_path(filename)}"
86
+ if opts[:draft]
87
+ plain "View drafts at the URL: /dash"
88
+ end
89
+ }
90
+ end
70
91
 
92
+ def ensure_unique_filename(original_filename, opts)
71
93
  begin
72
- file = @args[2] || "untitled"
94
+ file = original_filename || "untitled"
73
95
  ext = File.extname(file).to_s
74
96
  ext = ext.empty? ? @collection.config["ext"] : ext
75
97
 
@@ -78,7 +100,7 @@ module Ruhoh::Resources::Pages
78
100
  name = File.basename(file, ext).gsub(/\s+/, '-')
79
101
  File.join(File.dirname(file), name)
80
102
  else
81
- Ruhoh::Utils.to_slug(File.basename(file, ext))
103
+ Ruhoh::StringFormat.clean_slug(File.basename(file, ext))
82
104
  end
83
105
 
84
106
  name = "#{name}-#{@iterator}" unless @iterator.zero?
@@ -88,19 +110,7 @@ module Ruhoh::Resources::Pages
88
110
  @iterator += 1
89
111
  end while File.exist?(filename)
90
112
 
91
- FileUtils.mkdir_p File.dirname(filename)
92
- output = (@collection.scaffold || '').gsub('{{DATE}}', Time.now.strftime('%Y-%m-%d'))
93
-
94
- File.open(filename, 'w:UTF-8') {|f| f.puts output }
95
-
96
- resource_name = @collection.resource_name
97
- Ruhoh::Friend.say {
98
- green "New #{resource_name}:"
99
- green " > #{ruhoh.relative_path(filename)}"
100
- if opts[:draft]
101
- plain "View drafts at the URL: /dash"
102
- end
103
- }
113
+ filename
104
114
  end
105
115
 
106
116
  def _list(data)
@@ -121,4 +131,4 @@ module Ruhoh::Resources::Pages
121
131
  end
122
132
  end
123
133
  end
124
- end
134
+ end
@@ -72,6 +72,7 @@ module Ruhoh::Resources::Pages
72
72
  xml.rss(:version => '2.0') {
73
73
  xml.channel {
74
74
  xml.title_ data['title']
75
+ xml.description_ (data['description'] ? data['description'] : data['title'])
75
76
  xml.link_ @ruhoh.config['production_url']
76
77
  xml.pubDate_ Time.now
77
78
  pages.each do |page|
@@ -98,4 +99,4 @@ module Ruhoh::Resources::Pages
98
99
  }
99
100
  end
100
101
  end
101
- end
102
+ end
@@ -0,0 +1,9 @@
1
+ module Ruhoh::Resources::Static
2
+ class Collection
3
+ include Ruhoh::Base::Collectable
4
+
5
+ def url_endpoint
6
+ resource_name
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,33 @@
1
+ module Ruhoh::Resources::Static
2
+ class Compiler
3
+ include Ruhoh::Base::Compilable
4
+
5
+ # A basic compiler task which copies each valid collection resource file to the compiled folder.
6
+ # Valid files are identified by their pointers.
7
+ # Invalid files are files that are excluded from the resource's configuration settings.
8
+ # The collection's url_endpoint is used to determine the final compiled path.
9
+ #
10
+ # @returns Nothing.
11
+ def run
12
+ collection = @collection
13
+
14
+ unless @collection.paths?
15
+ Ruhoh::Friend.say { yellow "#{collection.resource_name.capitalize}: directory not found - skipping." }
16
+ return
17
+ end
18
+ Ruhoh::Friend.say { cyan "#{collection.resource_name.capitalize}: (copying valid files)" }
19
+
20
+ compiled_path = Ruhoh::Utils.url_to_path(@ruhoh.to_url(@collection.url_endpoint), @ruhoh.paths.compiled)
21
+ FileUtils.mkdir_p compiled_path
22
+
23
+ @collection.files.values.each do |pointer|
24
+ compiled_file = File.join(compiled_path, pointer['id'])
25
+
26
+ FileUtils.mkdir_p File.dirname(compiled_file)
27
+ FileUtils.cp_r pointer['realpath'], compiled_file
28
+
29
+ Ruhoh::Friend.say { green " > #{pointer['id']}" }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,51 @@
1
+ class Ruhoh
2
+ # StringFormat is meant to expose the common (public) interface
3
+ # for where strings (namely URLS) are formatted.
4
+ # Users are encouraged to reimplement these methods via plugins to enable
5
+ # custom-defined slug generation logic based on their tastes.
6
+ #
7
+ # TODO:
8
+ # - Natively support the most popular slug formats.
9
+ # - Better support for Internationalization.
10
+ module StringFormat
11
+
12
+ # Public interface for building 'clean slugs'
13
+ # Redefine this method to implement custom slug generation.
14
+ def self.clean_slug(string)
15
+ hyphenate(string)
16
+ end
17
+
18
+ def self.clean_slug_and_escape(string)
19
+ CGI::escape(clean_slug(string))
20
+ end
21
+
22
+ # Simple url slug normalization.
23
+ # Converts all non word characters into hyphens.
24
+ # This may not be what you want so feel free to overwite the public
25
+ # method in place of another formatter.
26
+ #
27
+ # Ex: My Post Title ===> my-post-title
28
+ def self.hyphenate(string)
29
+ string = string.to_s.downcase.strip.gsub(/[^\p{Word}+]/u, '-')
30
+ string.gsub(/^\-+/, '').gsub(/\-+$/, '').gsub(/\-+/, '-')
31
+ end
32
+
33
+ # TODO: Probably use ActiveSupport for this stuff
34
+ # Ex: my-post-title ===> My Post Title
35
+ def self.titleize(string)
36
+ string.gsub(/[^\p{Word}+]/u, ' ').gsub(/\b\w/){ $&.upcase }
37
+ end
38
+
39
+ # Convert CamelCase to snake_case
40
+ # Thanks ActiveSupport: http://stackoverflow.com/a/1509939/101940
41
+ def self.snake_case(string)
42
+ string.
43
+ to_s.
44
+ gsub(/::/, '/').
45
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
46
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
47
+ tr("-", "_").
48
+ downcase
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,65 @@
1
+ require 'nokogiri'
2
+ class Ruhoh
3
+ class Summarizer
4
+ SummaryNodeClassName = 'summary'
5
+ Headings = Nokogiri::HTML::ElementDescription::HEADING + %w{ header hgroup }
6
+
7
+ def initialize(opts)
8
+ @content = opts[:content] ; opts.delete(:content)
9
+ @opts = opts
10
+ end
11
+
12
+ # Generate a truncated summary.
13
+ # - If a summary element (`<tag class="summary">...</tag>`) is specified
14
+ # in the content, return it.
15
+ # - If summary_lines > 0, truncate after the first complete element where
16
+ # the number of summary lines is greater than summary_lines.
17
+ # - If @opts[:stop_at_header] is a number n, stop before the nth header.
18
+ # - If @opts[:stop_at_header] is true, stop before the first header after
19
+ # content has been included. In other words, don't count headers at the
20
+ # top of the page.
21
+ def generate
22
+ content_doc = Nokogiri::HTML.fragment(@content)
23
+
24
+ # Return a summary element if specified
25
+ summary_el = content_doc.at_css('.' + SummaryNodeClassName)
26
+ return summary_el.to_html if summary_el
27
+
28
+ # Create the summary element.
29
+ summary_doc = Nokogiri::XML::Node.new("div", Nokogiri::HTML::Document.new)
30
+ summary_doc["class"] = SummaryNodeClassName
31
+
32
+ content_doc.children.each do |node|
33
+
34
+ if @opts[:stop_at_header] == true
35
+ # Detect first header after content
36
+ if not (Headings.include?(node.name) && node.content.empty?)
37
+ @opts[:stop_at_header] = 1
38
+ end
39
+ elsif @opts[:stop_at_header].is_a?(Integer) && Headings.include?(node.name)
40
+ if @opts[:stop_at_header] > 1
41
+ @opts[:stop_at_header] -= 1;
42
+ else
43
+ summary_doc["class"] += " ellipsis"
44
+ break
45
+ end
46
+ end
47
+
48
+ if @opts[:line_limit] > 0 && summary_doc.content.lines.to_a.length > @opts[:line_limit]
49
+ # Skip through leftover whitespace. Without this check, the summary
50
+ # can be marked as ellipsis even if it isn't.
51
+ unless node.text? && node.text.strip.empty?
52
+ summary_doc["class"] += " ellipsis"
53
+ break
54
+ else
55
+ next
56
+ end
57
+ end
58
+
59
+ summary_doc << node
60
+ end
61
+
62
+ summary_doc.to_html
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,126 @@
1
+ class Ruhoh
2
+ class UrlSlug
3
+ def initialize(opts)
4
+ @page_data = opts[:page_data]
5
+ @format = opts[:format]
6
+ @pointer = @page_data["pointer"]
7
+ end
8
+
9
+ # @return[String] URL Slug based on the given data and format.
10
+ def generate
11
+ url = @format.include?(':') ? dynamic : literal
12
+ url = process_url_extension(url)
13
+ url.empty? ? '/' : url
14
+ end
15
+
16
+ # @return[String] the literal URL without token substitution.
17
+ def literal
18
+ @format.gsub(/^\//, '').split('/').map {|p| CGI::escape(p) }.join('/')
19
+ end
20
+
21
+ # @return[String] the dynamic URL with token substitution.
22
+ def dynamic
23
+ data.inject(@format) { |result, token|
24
+ result.gsub(/:#{ Regexp.escape(token.first) }/, token.last)
25
+ }.gsub(/\/+/, "/")
26
+ end
27
+
28
+ def data
29
+ result = uses_date? ? date_data : {}
30
+ result.merge({
31
+ "title" => title,
32
+ "filename" => filename,
33
+ "path" => path,
34
+ "relative_path" => relative_path,
35
+ "categories" => category,
36
+ })
37
+ end
38
+
39
+ def date_data
40
+ date = Time.parse(@page_data['date'].to_s)
41
+
42
+ {
43
+ "year" => date.strftime("%Y"),
44
+ "month" => date.strftime("%m"),
45
+ "day" => date.strftime("%d"),
46
+ "i_day" => date.strftime("%d").to_i.to_s,
47
+ "i_month" => date.strftime("%m").to_i.to_s,
48
+ }
49
+ rescue ArgumentError, TypeError
50
+ Ruhoh.log.error(
51
+ "ArgumentError:" +
52
+ " The file '#{ @pointer["realpath"] }' has a permalink '#{ @format }'" +
53
+ " which is date dependant but the date '#{ @page_data['date'] }' could not be parsed." +
54
+ " Ensure the date's format is: 'YYYY-MM-DD'"
55
+ )
56
+ end
57
+
58
+ def title
59
+ Ruhoh::StringFormat.clean_slug_and_escape(@page_data['title'])
60
+ end
61
+
62
+ def filename
63
+ File.basename(@page_data['id'], ext)
64
+ end
65
+
66
+ def ext
67
+ File.extname(@page_data['id'])
68
+ end
69
+
70
+ # Category is only the first one if multiple categories exist.
71
+ def category
72
+ string = Array(@page_data['categories'])[0]
73
+ return '' if string.to_s.empty?
74
+
75
+ string.split('/').map { |c|
76
+ Ruhoh::StringFormat.clean_slug_and_escape(c)
77
+ }.join('/')
78
+ end
79
+
80
+ def relative_path
81
+ string = File.dirname(@page_data['id'])
82
+ (string == ".") ? "" : string
83
+ end
84
+
85
+ def path
86
+ File.join(@pointer["resource"], relative_path)
87
+ end
88
+
89
+ private
90
+
91
+ def uses_date?
92
+ result = false
93
+ %w{ :year :month :day :i_day :i_month }.each do |token|
94
+ if @format.include?(token)
95
+ result = true
96
+ break
97
+ end
98
+ end
99
+
100
+ result
101
+ end
102
+
103
+ # Is an extension explicitly defined?
104
+ def uses_extension?
105
+ @format =~ /\.[^\.]+$/
106
+ end
107
+
108
+ # The url extension depends on multiple factors:
109
+ # user-config : preserve any extension set by the user in the format.
110
+ # converters : Automatically change convertable extensions to .html
111
+ # Non-convertable file-extensions should 'pass-through'
112
+ # 'pretty' : Automatically prettify urls (omit .html) unless user disabled.
113
+ #
114
+ # @return[String]
115
+ def process_url_extension(url)
116
+ return url if uses_extension?
117
+
118
+ url += Ruhoh::Converter.extensions.include?(ext) ? '.html' : ext
119
+
120
+ # Prettify by default
121
+ @page_data['permalink_ext'] ?
122
+ url :
123
+ url.gsub(/index|index.html$/, '').gsub(/\.html$/, '')
124
+ end
125
+ end
126
+ end
data/lib/ruhoh/utils.rb CHANGED
@@ -21,18 +21,7 @@ class Ruhoh
21
21
  parts = parts.unshift(base) if base
22
22
  File.__send__(:join, parts)
23
23
  end
24
-
25
- def self.to_url_slug(title)
26
- CGI::escape self.to_slug(title)
27
- end
28
-
29
- # My Post Title ===> my-post-title
30
- def self.to_slug(title)
31
- title = title.to_s.downcase.strip.gsub(/[^\p{Word}+]/u, '-')
32
- title.gsub(/^\-+/, '').gsub(/\-+$/, '').gsub(/\-+/, '-')
33
- end
34
-
35
-
24
+
36
25
  def self.report(name, collection, invalid)
37
26
  output = "#{collection.count}/#{collection.count + invalid.count} #{name} processed."
38
27
  if collection.empty? && invalid.empty?
@@ -73,17 +62,5 @@ class Ruhoh
73
62
 
74
63
  Object.module_eval("::#{$1}", __FILE__, __LINE__)
75
64
  end
76
-
77
- # Thanks ActiveSupport: http://stackoverflow.com/a/1509939/101940
78
- def self.underscore(string)
79
- string.
80
- to_s.
81
- gsub(/::/, '/').
82
- gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
83
- gsub(/([a-z\d])([A-Z])/,'\1_\2').
84
- tr("-", "_").
85
- downcase
86
- end
87
-
88
65
  end
89
- end #Ruhoh
66
+ end
data/lib/ruhoh/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  class Ruhoh
2
- Version = VERSION = '2.2'
2
+ Version = VERSION = '2.3'
3
3
  RuhohSpec = '2.1'
4
4
  end
@@ -45,7 +45,11 @@ module Ruhoh::Views
45
45
  end
46
46
 
47
47
  def urls
48
- @ruhoh.collections.url_endpoints
48
+ @ruhoh.collections.url_endpoints.merge({
49
+ 'base_path' => @ruhoh.base_path,
50
+ 'production' => @ruhoh.config["production_url"],
51
+ 'production_url' => @ruhoh.config["production_url"]
52
+ })
49
53
  end
50
54
 
51
55
  def content
@@ -93,7 +97,7 @@ module Ruhoh::Views
93
97
  # Handy for transforming ids into css-classes in your views.
94
98
  # @returns[String]
95
99
  def to_slug(sub_context)
96
- Ruhoh::Utils.to_slug(sub_context)
100
+ Ruhoh::StringFormat.clean_slug(sub_context)
97
101
  end
98
102
 
99
103
  # Public: Formats the path to the compiled file based on the URL.
data/lib/ruhoh.rb CHANGED
@@ -23,6 +23,8 @@ require 'ruhoh/views/master_view'
23
23
  require 'ruhoh/collections'
24
24
  require 'ruhoh/cache'
25
25
  require 'ruhoh/routes'
26
+ require 'ruhoh/string_format'
27
+ require 'ruhoh/url_slug'
26
28
  require 'ruhoh/programs/preview'
27
29
 
28
30
  class Ruhoh
@@ -60,7 +62,9 @@ class Ruhoh
60
62
  @collections.load(resource)
61
63
  end
62
64
 
63
- def config
65
+ def config(reload=false)
66
+ return @config unless (reload or @config.nil?)
67
+
64
68
  config = Ruhoh::Utils.parse_yaml_file(@base, "config.yml") || {}
65
69
  config['compiled'] = config['compiled'] ? File.expand_path(config['compiled']) : "compiled"
66
70
 
@@ -255,4 +259,4 @@ class Ruhoh
255
259
  def self.model(resource)
256
260
  Collections.get_module_namespace_for(resource).const_get(:ModelView)
257
261
  end
258
- end
262
+ end
@@ -1,6 +1,6 @@
1
1
  ---
2
- title:
2
+ title: '{{TITLE}}'
3
3
  date: '{{DATE}}'
4
4
  description:
5
5
  tags: []
6
- ---
6
+ ---
@@ -1,3 +1,4 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'compiler.rb')
1
2
  class Ruhoh::Resources::Stylesheets::Compiler
2
3
  include Ruhoh::SprocketsPlugin::Compiler
3
4
  end
@@ -1,3 +1,4 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'previewer.rb')
1
2
  module Ruhoh::Resources::Stylesheets
2
3
  class Previewer
3
4
  include Ruhoh::SprocketsPlugin::Previewer
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruhoh
3
3
  version: !ruby/object:Gem::Version
4
- version: '2.2'
4
+ version: '2.3'
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-06-09 00:00:00.000000000 Z
12
+ date: 2013-08-25 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
@@ -152,14 +152,17 @@ files:
152
152
  - bin/ruhoh
153
153
  - cucumber.yml
154
154
  - features/categories.feature
155
+ - features/config.feature
155
156
  - features/conversion.feature
156
157
  - features/data.feature
157
158
  - features/drafts.feature
159
+ - features/ignore.feature
158
160
  - features/javascripts.feature
159
161
  - features/layouts.feature
160
162
  - features/pagination.feature
161
163
  - features/partials.feature
162
164
  - features/permalinks.feature
165
+ - features/static.feature
163
166
  - features/step_defs.rb
164
167
  - features/stylesheets.feature
165
168
  - features/summary.feature
@@ -194,6 +197,7 @@ files:
194
197
  - lib/ruhoh/resources/dash/previewer.rb
195
198
  - lib/ruhoh/resources/data/collection.rb
196
199
  - lib/ruhoh/resources/data/collection_view.rb
200
+ - lib/ruhoh/resources/ignore/collection.rb
197
201
  - lib/ruhoh/resources/javascripts/collection.rb
198
202
  - lib/ruhoh/resources/javascripts/collection_view.rb
199
203
  - lib/ruhoh/resources/javascripts/compiler.rb
@@ -209,6 +213,8 @@ files:
209
213
  - lib/ruhoh/resources/pages/model_view.rb
210
214
  - lib/ruhoh/resources/pages/previewer.rb
211
215
  - lib/ruhoh/resources/partials/model.rb
216
+ - lib/ruhoh/resources/static/collection.rb
217
+ - lib/ruhoh/resources/static/compiler.rb
212
218
  - lib/ruhoh/resources/stylesheets/collection.rb
213
219
  - lib/ruhoh/resources/stylesheets/collection_view.rb
214
220
  - lib/ruhoh/resources/stylesheets/compiler.rb
@@ -219,6 +225,9 @@ files:
219
225
  - lib/ruhoh/resources/widgets/compiler.rb
220
226
  - lib/ruhoh/resources/widgets/model.rb
221
227
  - lib/ruhoh/routes.rb
228
+ - lib/ruhoh/string_format.rb
229
+ - lib/ruhoh/summarizer.rb
230
+ - lib/ruhoh/url_slug.rb
222
231
  - lib/ruhoh/utils.rb
223
232
  - lib/ruhoh/version.rb
224
233
  - lib/ruhoh/views/helpers/categories.rb
@@ -267,7 +276,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
267
276
  version: '0'
268
277
  segments:
269
278
  - 0
270
- hash: 3919672923865862161
279
+ hash: 897888631782695949
271
280
  required_rubygems_version: !ruby/object:Gem::Requirement
272
281
  none: false
273
282
  requirements:
@@ -276,7 +285,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
276
285
  version: '0'
277
286
  segments:
278
287
  - 0
279
- hash: 3919672923865862161
288
+ hash: 897888631782695949
280
289
  requirements: []
281
290
  rubyforge_project:
282
291
  rubygems_version: 1.8.24