ro 1.4.6 → 4.2.0

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