ro 1.4.6 → 4.2.0

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 (66) hide show
  1. checksums.yaml +6 -14
  2. data/Gemfile +2 -0
  3. data/Gemfile.lock +43 -0
  4. data/LICENSE +1 -0
  5. data/README.md +120 -112
  6. data/README.md.erb +159 -0
  7. data/Rakefile +129 -78
  8. data/bin/ro +241 -68
  9. data/lib/ro/_lib.rb +95 -0
  10. data/lib/ro/asset.rb +45 -0
  11. data/lib/ro/collection/list.rb +23 -0
  12. data/lib/ro/collection.rb +168 -0
  13. data/lib/ro/config.rb +68 -0
  14. data/lib/ro/error.rb +8 -0
  15. data/lib/ro/klass.rb +25 -0
  16. data/lib/ro/methods.rb +238 -0
  17. data/lib/ro/model.rb +83 -114
  18. data/lib/ro/node.rb +177 -360
  19. data/lib/ro/pagination.rb +7 -4
  20. data/lib/ro/path.rb +225 -0
  21. data/lib/ro/root.rb +52 -31
  22. data/lib/ro/script/builder.rb +221 -0
  23. data/lib/ro/script/console.rb +47 -0
  24. data/lib/ro/script/server.rb +76 -0
  25. data/lib/ro/script.rb +189 -0
  26. data/lib/ro/slug.rb +19 -18
  27. data/lib/ro/template/rouge_formatter.rb +42 -0
  28. data/lib/ro/template.rb +104 -50
  29. data/lib/ro.rb +85 -317
  30. data/public/api/ro/index-1.json +147 -0
  31. data/public/api/ro/index.json +137 -0
  32. data/public/api/ro/posts/first_post/index.json +52 -0
  33. data/public/api/ro/posts/index-1.json +145 -0
  34. data/public/api/ro/posts/index.json +135 -0
  35. data/public/api/ro/posts/second_post/index.json +51 -0
  36. data/public/api/ro/posts/third_post/index.json +51 -0
  37. data/public/ro/posts/first_post/assets/foo/bar/baz.jpg +0 -0
  38. data/public/ro/posts/first_post/assets/foo.jpg +0 -0
  39. data/public/ro/posts/first_post/assets/src/foo/bar.rb +3 -0
  40. data/public/ro/posts/first_post/attributes.yml +2 -0
  41. data/public/ro/posts/first_post/blurb.erb.md +7 -0
  42. data/public/ro/posts/first_post/body.md +16 -0
  43. data/public/ro/posts/first_post/testing.txt +3 -0
  44. data/public/ro/posts/second_post/assets/foo/bar/baz.jpg +0 -0
  45. data/public/ro/posts/second_post/assets/foo.jpg +0 -0
  46. data/public/ro/posts/second_post/assets/src/foo/bar.rb +3 -0
  47. data/public/ro/posts/second_post/attributes.yml +2 -0
  48. data/public/ro/posts/second_post/blurb.erb.md +5 -0
  49. data/public/ro/posts/second_post/body.md +16 -0
  50. data/public/ro/posts/third_post/assets/foo/bar/baz.jpg +0 -0
  51. data/public/ro/posts/third_post/assets/foo.jpg +0 -0
  52. data/public/ro/posts/third_post/assets/src/foo/bar.rb +3 -0
  53. data/public/ro/posts/third_post/attributes.yml +2 -0
  54. data/public/ro/posts/third_post/blurb.erb.md +5 -0
  55. data/public/ro/posts/third_post/body.md +16 -0
  56. data/ro.gemspec +89 -29
  57. metadata +106 -90
  58. data/TODO.md +0 -50
  59. data/lib/ro/blankslate.rb +0 -7
  60. data/lib/ro/cache.rb +0 -26
  61. data/lib/ro/git.rb +0 -374
  62. data/lib/ro/initializers/env.rb +0 -5
  63. data/lib/ro/initializers/tilt.rb +0 -104
  64. data/lib/ro/lock.rb +0 -53
  65. data/lib/ro/node/list.rb +0 -142
  66. data/notes/ara.txt +0 -215
data/lib/ro/node.rb CHANGED
@@ -1,478 +1,295 @@
1
1
  module Ro
2
2
  class Node
3
- fattr :root
4
- fattr :path
5
- fattr :type
6
- fattr :loaded
7
- fattr :fields
3
+ include Klass
4
+
5
+ attr_reader :path, :root
8
6
 
9
7
  def initialize(path)
10
- @path = Ro.realpath(path.to_s)
11
- @id = File.basename(@path)
12
- @slug = Slug.for(@id)
13
- @type = File.basename(File.dirname(@path))
14
- @root = Ro::Root.new(File.dirname(File.dirname(@path)))
15
- @loaded = false
16
- @loading = false
17
- @attributes = Map.new
18
- @fields = Map.new
8
+ @path = Path.for(path)
9
+ @root = Root.for(@path.parent.parent)
10
+ @attributes = :lazyload
19
11
  end
20
12
 
21
- def id
22
- @id
13
+ def name
14
+ @path.name
23
15
  end
24
16
 
25
- def _id
26
- @id
17
+ def id
18
+ name
27
19
  end
28
20
 
29
21
  def type
30
- attributes[:type] || @type
31
- end
32
-
33
- def _type
34
- @type
35
- end
36
-
37
- def path
38
- attributes[:path] || @path
39
- end
40
-
41
- def _path
42
- @path
43
- end
44
-
45
- def slug
46
- attributes[:slug] || @slug
47
- end
48
-
49
- def _slug
50
- @slug
22
+ @path.parent.name
51
23
  end
52
24
 
53
25
  def identifier
54
- "#{ _type }/#{ _id }"
55
- end
56
-
57
- def hash
58
- identifier.hash
59
- end
60
-
61
- def ==(other)
62
- attributes == other.attributes
26
+ File.join(type, id)
63
27
  end
64
28
 
65
29
  def inspect
66
30
  identifier
67
31
  end
68
32
 
69
- def to_s
70
- inspect
33
+ def collection
34
+ @root.collection_for(type)
71
35
  end
72
36
 
73
- def basename
74
- name
75
- end
76
-
77
- def node
78
- self
79
- end
80
-
81
- def get(*args)
82
- attributes.get(*args)
37
+ def attributes
38
+ load_attributes
39
+ @attributes
83
40
  end
84
41
 
85
- def [](*args)
86
- attributes.get(*args)
42
+ def load_attributes
43
+ load_attributes! if @attributes == :lazyload
87
44
  end
88
45
 
89
- def asset_path(*args, &block)
90
- File.join(relative_path, 'assets')
91
- end
46
+ def load_attributes!
47
+ @attributes = Map.new
92
48
 
93
- def asset_dir
94
- File.join(path, 'assets')
95
- end
49
+ _load_base_attributes
50
+ _load_asset_attributes
51
+ _load_meta_attributes
52
+ _load_file_attributes
96
53
 
97
- def asset_paths
98
- Dir.glob("#{ asset_dir }/**/**").select{|entry| test(?f, entry)}
54
+ @attributes
99
55
  end
100
56
 
101
- def assets
102
- asset_paths.map do |path|
103
- name = path.sub(asset_dir + "/", "")
104
- path_info = path.gsub(/^#{ Regexp.escape(Ro.root) }/, '')
105
- url = File.join(Ro.route, path_info)
106
- Asset.new(name, :path => path, :url => url)
107
- end
108
- end
109
-
110
- def asset_urls
111
- assets.map(&:url)
112
- end
113
-
114
- def asset_for(*args, &block)
115
- options = Map.options_for!(args)
116
-
117
- path_info = Ro.relative_path_for(args)
118
-
119
- path = File.join(@path.to_s, 'assets', path_info)
120
-
121
- glob = path_info.gsub(/[_-]/, '[_-]')
122
-
123
- globs =
124
- [
125
- File.join(@path.to_s, 'assets', "#{ glob }"),
126
- File.join(@path.to_s, 'assets', "#{ glob }*"),
127
- File.join(@path.to_s, 'assets', "**/#{ glob }*")
57
+ def _load_base_attributes
58
+ disallowed =
59
+ %w[
60
+ assets
61
+ _meta
128
62
  ]
129
63
 
130
- candidates = globs.map{|glob| Dir.glob(glob, ::File::FNM_CASEFOLD)}.flatten.compact.uniq
64
+ glob =
65
+ "attributes.{yml,yaml,json}"
131
66
 
132
- case candidates.size
133
- when 0
134
- raise ArgumentError.new("no asset matching #{ globs.inspect }")
135
- else
136
- path = candidates.first
137
- name = path.sub(asset_dir + "/", "")
138
- path_info = path.gsub(/^#{ Regexp.escape(Ro.root) }/, '')
139
- url = File.join(Ro.route, path_info)
140
- end
67
+ @path.glob(glob) do |file|
68
+ attrs = _render(file)
141
69
 
142
- Asset.new(name, :path => path, :url => url)
143
- end
70
+ disallowed.each do |key|
71
+ Ro.error!("#{ file } must not contain the key #{key.inspect}") if attrs.has_key?(key)
72
+ end
144
73
 
145
- def asset_for?(*args, &block)
146
- begin
147
- asset_for(*args, &block)
148
- rescue
149
- nil
74
+ @attributes.update(attrs)
150
75
  end
151
76
  end
152
77
 
153
- class Asset < ::String
154
- fattr(:path)
155
- fattr(:url)
156
-
157
- def initialize(name, options = {})
158
- super(name)
159
- ensure
160
- options = Map.for(options)
161
78
 
162
- Asset.fattrs.each do |attr|
163
- if options.has_key?(attr)
164
- value = options[attr]
165
- send(attr, value)
166
- end
79
+ def _load_asset_attributes
80
+ {}.tap do |hash|
81
+ assets.each do |asset|
82
+ key = asset.name
83
+ value = { url: asset.url, path: asset.path.relative_to(@root), src: asset.src }
84
+ hash[key] = value
167
85
  end
168
- end
169
86
 
170
- def name
171
- self
172
- end
173
-
174
- IMAGE_RE = %r/[.](jpg|jpeg|png|gif|tif|tiff)$/i
175
-
176
- def image?
177
- !!(self =~ IMAGE_RE)
87
+ @attributes.set(assets: hash)
178
88
  end
89
+ end
179
90
 
180
- def extension
181
- base, ext = basename.split('.', 2)
182
- ext
183
- end
184
- alias_method(:ext, :extension)
91
+ def _load_meta_attributes
92
+ {}.tap do |hash|
93
+ hash.update(
94
+ identifier:,
95
+ type:,
96
+ id:,
97
+ urls:
98
+ )
185
99
 
186
- def basename
187
- File.basename(path.to_s)
100
+ @attributes.set(_meta: hash)
188
101
  end
189
102
  end
190
103
 
191
- def url_for(*args, &block)
192
- options = Map.options_for!(args)
193
-
194
- opts = Map.new
104
+ def _load_file_attributes
105
+ ignored = _ignored_files
195
106
 
196
- opts[:timestamp] = options.delete(:timestamp)
107
+ @path.files.each do |file|
108
+ next if ignored.include?(file)
197
109
 
198
- args.push(options)
110
+ rel = file.relative_to(@path)
199
111
 
200
- asset = asset_for(*args, &block)
112
+ key = rel.parts
113
+ basename = key.pop
114
+ base = basename.split('.', 2).first
115
+ key.push(base)
201
116
 
202
- if ts = opts.delete(:timestamp)
203
- if ts == true
204
- opts[:_] = File.stat(asset.path).mtime.utc.to_i
205
- else
206
- opts[:_] = ts
117
+ if @attributes.has?(key)
118
+ raise Error.new("#{ @path } clobbers #{ key.inspect }!")
207
119
  end
208
- end
209
120
 
210
- if opts.empty?
211
- asset.url
212
- else
213
- query_string = Ro.query_string_for(opts)
214
- "#{ asset.url }?#{ query_string }"
215
- end
216
- end
121
+ value = _render(file)
217
122
 
218
- def url_for?(*args, &block)
219
- begin
220
- url_for(*args, &block)
221
- rescue
222
- nil
123
+ @attributes.set(key => value)
223
124
  end
224
125
  end
225
126
 
226
- def source_for(*args)
227
- key = Ro.relative_path_for(:assets, :source, args).split('/')
228
- get(key)
229
- end
230
-
231
- def route(*args)
232
- path_info = Ro.absolute_path_for(Ro.route, relative_path)
233
- [Ro.asset_host, path_info].compact.join('/')
234
- end
127
+ def _ignored_files
128
+ ignored_files =
129
+ %w[
130
+ attributes.yml
131
+ attributes.yaml
132
+ attributes.json
133
+ ./assets/**/**
134
+ ].map do |glob|
135
+ @path.glob(glob).select(&:file?)
136
+ end
235
137
 
236
- def relative_path
237
- re = /^#{ Regexp.escape(Ro.root) }/
238
- @path.to_s.gsub(re, '')
138
+ ignored_files.flatten
239
139
  end
240
140
 
241
- def method_missing(method, *args, &block)
242
- super if method.to_s == 'to_ary'
243
-
244
- Ro.log "Ro::Node(#{ identifier })#method_missing(#{ method.inspect }, #{ args.inspect })"
245
-
246
- key = method.to_s
141
+ def _render(file)
142
+ value = Ro.render(file, _render_context)
247
143
 
248
- if @attributes.has_key?(key)
249
- return @attributes[key]
144
+ if value.is_a?(Ro::Template::HTML)
145
+ html = value
146
+ value = Ro.expand_asset_urls(html, self)
250
147
  end
251
148
 
252
- _load do
253
- return(
254
- if @attributes.has_key?(key)
255
- @attributes[key]
256
- else
257
- super
258
- end
259
- )
260
- end
149
+ value
261
150
  end
262
151
 
263
- def attributes
264
- _load{ @attributes }
152
+ def _render_context
153
+ to_hash.tap do |context|
154
+ context[:ro] ||= root
155
+ context[:collection] ||= collection
156
+ end
265
157
  end
266
158
 
267
- def attributes=(attributes)
268
- @attributes = attributes
269
- end
270
-
271
- def instance_eval(*args, &block)
272
- _load{ super }
159
+ def get(*args)
160
+ attributes.get(*args)
273
161
  end
274
162
 
275
- def related(*args, &block)
276
- _load{
277
- related = @attributes.get(:related) || Map.new
278
- nodes = List.new(root)
279
- list = root.nodes
280
- which = Coerce.list_of_strings(args)
281
-
282
- related.each do |relationship, value|
283
- unless which.empty?
284
- next unless which.include?(relationship.to_s)
285
- end
286
-
287
- type, names =
288
- case value
289
- when Hash
290
- value.to_a.first
291
- else
292
- [relationship, value]
293
- end
294
-
295
- names = Coerce.list_of_strings(names)
296
-
297
- names.each do |name|
298
- identifier = "#{ type }/#{ name }"
299
- node = list.index[identifier]
300
- node._load{ nodes.add(node) }
301
- end
302
- end
303
-
304
- case
305
- when block.nil?
306
- nodes
307
-
308
- when block
309
- nodes.where(&block)
310
- end
311
- }
163
+ def [](*args)
164
+ attributes.get(*args)
312
165
  end
313
166
 
314
- def reload(&block)
315
- @loaded = false
316
- _load(&block)
167
+ def relative_path
168
+ path.relative_to(root)
317
169
  end
318
170
 
319
- def loaded
320
- attributes
171
+ def asset_dir
172
+ path.join('assets')
321
173
  end
322
174
 
323
- def load!(&block)
324
- _load(&block)
175
+ def asset_paths
176
+ asset_dir.select { |entry| entry.file? }.sort
325
177
  end
326
178
 
327
- def _load(&block)
328
- unless @loaded
329
- if @loading
330
- return(block ? block.call : :loading)
331
- end
332
-
333
- @loading = true
334
- @loaded = _load_from_cache_or_disk
335
- @loading = false
336
- end
337
-
338
- block ? block.call : @loaded
179
+ def assets
180
+ asset_paths.map { |path| Asset.for(path, node: self) }
339
181
  end
340
182
 
341
- def _load_from_cache_or_disk
342
- cache_key = _cache_key
343
-
344
- cached = Ro.cache.read(cache_key)
183
+ def asset_urls
184
+ assets.map(&:url)
185
+ end
345
186
 
346
- if cached
347
- Ro.log "loading #{ identifier } from cache"
187
+ def asset_for(*args)
188
+ options = Map.options_for!(args)
348
189
 
349
- @attributes = Map.new.update(cached)
190
+ path_info = Path.relative(args)
350
191
 
351
- return :cache
352
- else
353
- Ro.log "loading #{ identifier } from disk"
192
+ path = @path.join('assets', path_info)
354
193
 
355
- @attributes = Map.new
194
+ glob = path_info.gsub(/[_-]/, '[_-]')
356
195
 
357
- _load_attributes_yml
358
- _load_attribute_files
359
- _load_sources
360
- _load_assets
196
+ globs =
197
+ [
198
+ @path.call('assets', "#{glob}"),
199
+ @path.call('assets', "#{glob}*"),
200
+ @path.call('assets', "**/#{glob}*")
201
+ ]
361
202
 
362
- Ro.cache.write(cache_key, @attributes)
203
+ candidates = globs.map { |glob| Dir.glob(glob, ::File::FNM_CASEFOLD) }.flatten.compact.uniq.sort
363
204
 
364
- return :disk
205
+ case candidates.size
206
+ when 0
207
+ raise ArgumentError, "no asset matching #{globs.inspect}"
208
+ else
209
+ path = candidates.last
365
210
  end
366
- end
367
-
368
- def _load_attributes_yml
369
- if test(?s, _attributes_yml)
370
- buf = IO.binread(_attributes_yml)
371
- data = YAML.load(buf)
372
- data = data.is_a?(Hash) ? data : {'_' => data}
373
-
374
- @attributes.update(data)
375
-
376
- %w( assets ).each do |key|
377
- raise ArgumentError.new("attributes.yml may not contain the key '#{ key }'") if @attributes.has_key?(key)
378
- end
379
211
 
380
- @attributes
381
- end
212
+ Asset.for(path, node: self)
382
213
  end
383
214
 
384
- def _load_attribute_files
385
- glob = File.join(@path, '**/**')
386
- node = self
387
-
388
- Dir.glob(glob) do |path|
389
- next if test(?d, path)
390
-
391
- basename = File.basename(path)
392
- next if basename == 'attributes.yml'
393
-
394
- relative_path = Ro.relative_path(path, :to => @path)
395
- next if relative_path =~ /^assets\//
215
+ def asset_for?(*args, &block)
216
+ asset_for(*args, &block)
217
+ rescue StandardError
218
+ nil
219
+ end
396
220
 
397
- key = relative_path.split('.', 2).first.split('/')
221
+ def url_for(relative_path, options = {})
222
+ raise ArgumentError, relative_path if Path.absolute?(relative_path)
398
223
 
399
- html = Ro.render(path, node)
400
- html = Ro.expand_asset_urls(html, node)
224
+ fullpath = Path.for(path, relative_path).expand
225
+ raise ArgumentError, "#{relative_path.inspect} -- DOES NOT EXIST" unless fullpath.exist?
401
226
 
402
- @attributes.set(key => html)
403
- end
227
+ Ro.url_for(self.relative_path, relative_path, options)
404
228
  end
405
229
 
406
- def _load_sources
407
- glob = File.join(@path, 'assets/source/*')
408
- node = self
409
-
410
- Dir.glob(glob) do |path|
411
- next if test(?d, path)
412
-
413
- basename = File.basename(path)
414
- key, ext = basename.split('.', 2)
230
+ def src_for(*args)
231
+ key = Path.relative(:assets, :src, args).split('/')
232
+ get(key)
233
+ end
415
234
 
416
- next if basename == 'attributes.yml'
235
+ def method_missing(method, *args, &block)
236
+ key = method.to_s
417
237
 
418
- value = Ro.render_source(path, node)
419
- @attributes.set([:assets, :source, basename] => value)
238
+ if attributes.has_key?(key)
239
+ attributes[key]
240
+ else
241
+ super
420
242
  end
421
243
  end
422
244
 
423
- def _load_assets
424
- glob = File.join(@path, 'assets/**/**')
425
- node = self
426
-
427
- Dir.glob(glob) do |path|
428
- next if test(?d, path)
429
-
430
- relative_path = Ro.relative_path(path, :to => "#{ @path }/assets")
431
-
432
- url = url_for(relative_path)
433
- key = relative_path.split('/')
434
-
435
- key.unshift('urls')
245
+ def to_hash
246
+ attributes.to_hash
247
+ end
436
248
 
437
- @attributes.set(key => url)
438
- end
249
+ def to_s(...)
250
+ to_json(...)
439
251
  end
440
252
 
441
- def _binding
442
- Kernel.binding
253
+ def to_json(...)
254
+ JSON.pretty_generate(to_hash, ...)
443
255
  end
444
256
 
445
- def _cache_key
446
- glob = File.join(@path, '**/**')
257
+ def as_json(...)
258
+ to_hash.as_json(...)
259
+ end
447
260
 
448
- entries = []
261
+ def to_yaml(...)
262
+ to_hash.to_yaml(...)
263
+ end
449
264
 
450
- Dir.glob(glob) do |entry|
451
- stat =
452
- begin
453
- File.stat(entry)
454
- rescue
455
- next
456
- end
265
+ def _mapify(data)
266
+ converted = 'this_recursively_converts_nested_hashes_into_maps'
457
267
 
458
- timestamp = [stat.ctime, stat.mtime].max
459
- relative_path = Ro.relative_path(entry, :to => @path)
460
- entries.push([relative_path, timestamp.iso8601(2)])
461
- end
268
+ Map.for(converted => data)[converted]
269
+ end
462
270
 
463
- fingerprint = entries.map{|pair| pair.join('@')}.join(', ')
271
+ def files
272
+ path.glob('**/**').select { |entry| entry.file? }.sort
273
+ end
464
274
 
465
- md5 = Ro.md5(fingerprint)
275
+ def urls
276
+ files.map { |file| url_for(file.relative_to(@path)) }.sort
277
+ end
466
278
 
467
- [@path, md5]
279
+ def <=>(other)
280
+ sort_key <=> other.sort_key
468
281
  end
469
282
 
470
- def _load_from_cache
471
- false
283
+ def sort_key
284
+ default_sort_key
472
285
  end
473
286
 
474
- def _attributes_yml
475
- File.join(@path, 'attributes.yml')
287
+ def default_sort_key
288
+ position = (attributes[:position] ? Float(attributes[:position]) : 0.0)
289
+ published_at = (attributes[:published_at] ? Time.parse(attributes[:published_at].to_s) : Time.at(0)).utc.iso8601
290
+ created_at = (attributes[:created_at] ? Time.parse(attributes[:created_at].to_s) : Time.at(0)).utc.iso8601
291
+
292
+ [position, published_at, created_at, name]
476
293
  end
477
294
  end
478
295
  end
data/lib/ro/pagination.rb CHANGED
@@ -8,9 +8,9 @@ module Ro
8
8
 
9
9
  page = [page.abs, 1].max
10
10
  per = [per.abs, 1].max
11
-
11
+ d
12
12
  offset = (page - 1) * per
13
- length = per
13
+ length = per
14
14
 
15
15
  slice = dup.slice(offset, length)
16
16
  slice.page = page
@@ -23,12 +23,13 @@ module Ro
23
23
  @page = page
24
24
  end
25
25
 
26
- def page(*args)
26
+ def page(*_args)
27
+ paginate unless @page
27
28
  @page
28
29
  end
29
30
 
30
31
  def current_page
31
- @page
32
+ page
32
33
  end
33
34
 
34
35
  def per=(per)
@@ -36,6 +37,7 @@ module Ro
36
37
  end
37
38
 
38
39
  def per
40
+ paginate unless @per
39
41
  @per
40
42
  end
41
43
 
@@ -44,6 +46,7 @@ module Ro
44
46
  end
45
47
 
46
48
  def num_pages
49
+ paginate unless @num_pages
47
50
  @num_pages
48
51
  end
49
52