ro 1.4.6 → 4.4.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 (157) hide show
  1. checksums.yaml +6 -14
  2. data/Gemfile +2 -0
  3. data/Gemfile.lock +64 -0
  4. data/LICENSE +1 -0
  5. data/README.md +276 -105
  6. data/README.md.erb +159 -0
  7. data/Rakefile +129 -78
  8. data/bin/ro +241 -68
  9. data/lib/ro/_lib.rb +107 -0
  10. data/lib/ro/asset.rb +55 -0
  11. data/lib/ro/collection/list.rb +23 -0
  12. data/lib/ro/collection.rb +211 -0
  13. data/lib/ro/config.rb +72 -0
  14. data/lib/ro/error.rb +11 -0
  15. data/lib/ro/html.rb +23 -0
  16. data/lib/ro/html_safe.rb +143 -0
  17. data/lib/ro/klass.rb +25 -0
  18. data/lib/ro/methods.rb +295 -0
  19. data/lib/ro/model.rb +83 -114
  20. data/lib/ro/node.rb +209 -349
  21. data/lib/ro/pagination.rb +7 -4
  22. data/lib/ro/path.rb +229 -0
  23. data/lib/ro/root.rb +52 -31
  24. data/lib/ro/script/builder.rb +221 -0
  25. data/lib/ro/script/console.rb +47 -0
  26. data/lib/ro/script/server.rb +76 -0
  27. data/lib/ro/script.rb +189 -0
  28. data/lib/ro/slug.rb +19 -18
  29. data/lib/ro/template/rouge_formatter.rb +42 -0
  30. data/lib/ro/template.rb +145 -51
  31. data/lib/ro/text.rb +120 -0
  32. data/lib/ro.rb +89 -317
  33. data/public/api/ro/index-1.json +1065 -0
  34. data/public/api/ro/index.json +1055 -0
  35. data/public/api/ro/nerd/fastest-possible-embeddings/index.json +90 -0
  36. data/public/api/ro/nerd/ima/index.json +49 -0
  37. data/public/api/ro/nerd/index/index.json +74 -0
  38. data/public/api/ro/nerd/index-1.json +204 -0
  39. data/public/api/ro/nerd/index.json +194 -0
  40. data/public/api/ro/pages/about/index.json +60 -0
  41. data/public/api/ro/pages/contact/index.json +50 -0
  42. data/public/api/ro/pages/cv/index.json +49 -0
  43. data/public/api/ro/pages/disco/index.json +117 -0
  44. data/public/api/ro/pages/index/index.json +30 -0
  45. data/public/api/ro/pages/index-1.json +366 -0
  46. data/public/api/ro/pages/index.json +356 -0
  47. data/public/api/ro/pages/jess/index.json +62 -0
  48. data/public/api/ro/pages/now/index.json +43 -0
  49. data/public/api/ro/posts/almost-died-in-an-ice-cave/index.json +265 -0
  50. data/public/api/ro/posts/facebook-and-global-extremism/index.json +90 -0
  51. data/public/api/ro/posts/index-1.json +527 -0
  52. data/public/api/ro/posts/index.json +517 -0
  53. data/public/api/ro/posts/lemmings-considered-harmful/index.json +49 -0
  54. data/public/api/ro/posts/lost-in-the-desert/index.json +49 -0
  55. data/public/api/ro/posts/mission/index.json +49 -0
  56. data/public/api/ro/posts/return-your-laptop/index.json +61 -0
  57. data/public/ro/nerd/fastest-possible-embeddings/assets/giraffe.jpeg +0 -0
  58. data/public/ro/nerd/fastest-possible-embeddings/assets/let-me-in.jpg +0 -0
  59. data/public/ro/nerd/fastest-possible-embeddings/assets/src/fastembed.js +70 -0
  60. data/public/ro/nerd/fastest-possible-embeddings/assets/src/fastembed.rs +68 -0
  61. data/public/ro/nerd/fastest-possible-embeddings/assets/terminal.jpg +0 -0
  62. data/public/ro/nerd/fastest-possible-embeddings/attributes.yml +7 -0
  63. data/public/ro/nerd/fastest-possible-embeddings/body.md +266 -0
  64. data/public/ro/nerd/ima/assets/og.jpeg +0 -0
  65. data/public/ro/nerd/ima/attributes.yml +8 -0
  66. data/public/ro/nerd/ima/body.md +22 -0
  67. data/public/ro/nerd/index/assets/giraffe.jpeg +0 -0
  68. data/public/ro/nerd/index/assets/let-me-in.jpg +0 -0
  69. data/public/ro/nerd/index/assets/terminal.jpg +0 -0
  70. data/public/ro/nerd/index/attributes.yml +7 -0
  71. data/public/ro/nerd/index/body.md +130 -0
  72. data/public/ro/pages/about/assets/og.jpeg +0 -0
  73. data/public/ro/pages/about/assets/speak-english-pulp-fiction.gif +0 -0
  74. data/public/ro/pages/about/body.md +40 -0
  75. data/public/ro/pages/contact/assets/giraffe.jpeg +0 -0
  76. data/public/ro/pages/contact/attributes.yml +7 -0
  77. data/public/ro/pages/contact/body.md +9 -0
  78. data/public/ro/pages/cv/assets/ara.jpg +0 -0
  79. data/public/ro/pages/cv/attributes.yml +6 -0
  80. data/public/ro/pages/cv/body.md +122 -0
  81. data/public/ro/pages/disco/assets/disco.jpg +0 -0
  82. data/public/ro/pages/disco/assets/disco.png +0 -0
  83. data/public/ro/pages/disco/assets/speak-english-pulp-fiction.gif +0 -0
  84. data/public/ro/pages/disco/assets/src/environment.md +2354 -0
  85. data/public/ro/pages/disco/assets/src/fortune-500.md +2518 -0
  86. data/public/ro/pages/disco/assets/src/greed.md +2703 -0
  87. data/public/ro/pages/disco/assets/src/up-at-night.md +2337 -0
  88. data/public/ro/pages/disco/attributes.yml +9 -0
  89. data/public/ro/pages/disco/body.md +99 -0
  90. data/public/ro/pages/disco/samples/environment.md +2354 -0
  91. data/public/ro/pages/disco/samples/fortune-500.md +2518 -0
  92. data/public/ro/pages/disco/samples/greed.md +2703 -0
  93. data/public/ro/pages/disco/samples/up-at-night.md +2337 -0
  94. data/public/ro/pages/index/attributes.yml +1 -0
  95. data/public/ro/pages/index/body.md +15 -0
  96. data/public/ro/pages/jess/assets/og.jpg +0 -0
  97. data/public/ro/pages/jess/assets/speak-english-pulp-fiction.gif +0 -0
  98. data/public/ro/pages/jess/attributes.yml +7 -0
  99. data/public/ro/pages/jess/body.md +3 -0
  100. data/public/ro/pages/now/assets/speak-english-pulp-fiction.gif +0 -0
  101. data/public/ro/pages/now/attributes.yml +1 -0
  102. data/public/ro/pages/now/body.md +24 -0
  103. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image1.png +0 -0
  104. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image10.png +0 -0
  105. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image11.png +0 -0
  106. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image12.png +0 -0
  107. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image13.png +0 -0
  108. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image14.png +0 -0
  109. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image15.png +0 -0
  110. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image2.png +0 -0
  111. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image3.png +0 -0
  112. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image4.png +0 -0
  113. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image5.png +0 -0
  114. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image6.png +0 -0
  115. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image7.png +0 -0
  116. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image8.png +0 -0
  117. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image9.png +0 -0
  118. data/public/ro/posts/almost-died-in-an-ice-cave/assets/josh-pointing.jpg +0 -0
  119. data/public/ro/posts/almost-died-in-an-ice-cave/assets/levi-rawr.png +0 -0
  120. data/public/ro/posts/almost-died-in-an-ice-cave/assets/og.jpg +0 -0
  121. data/public/ro/posts/almost-died-in-an-ice-cave/assets/purple-heart.jpg +0 -0
  122. data/public/ro/posts/almost-died-in-an-ice-cave/attributes.yml +6 -0
  123. data/public/ro/posts/almost-died-in-an-ice-cave/body.md +419 -0
  124. data/public/ro/posts/facebook-and-global-extremism/assets/background.html +125 -0
  125. data/public/ro/posts/facebook-and-global-extremism/assets/background.md +95 -0
  126. data/public/ro/posts/facebook-and-global-extremism/assets/og.jpg +0 -0
  127. data/public/ro/posts/facebook-and-global-extremism/assets/prompt.txt +122 -0
  128. data/public/ro/posts/facebook-and-global-extremism/assets/results.md +183 -0
  129. data/public/ro/posts/facebook-and-global-extremism/assets/survey.txt +190 -0
  130. data/public/ro/posts/facebook-and-global-extremism/attributes.yml +7 -0
  131. data/public/ro/posts/facebook-and-global-extremism/body.md +393 -0
  132. data/public/ro/posts/lemmings-considered-harmful/assets/lemming.jpeg +0 -0
  133. data/public/ro/posts/lemmings-considered-harmful/attributes.yml +6 -0
  134. data/public/ro/posts/lemmings-considered-harmful/body.md +43 -0
  135. data/public/ro/posts/lost-in-the-desert/assets/og.jpg +0 -0
  136. data/public/ro/posts/lost-in-the-desert/attributes.yml +6 -0
  137. data/public/ro/posts/lost-in-the-desert/body.md +7 -0
  138. data/public/ro/posts/mission/assets/og.jpg +0 -0
  139. data/public/ro/posts/mission/attributes.yml +6 -0
  140. data/public/ro/posts/mission/body.md +4 -0
  141. data/public/ro/posts/return-your-laptop/assets/og.jpg +0 -0
  142. data/public/ro/posts/return-your-laptop/assets/return-your-laptop.png +0 -0
  143. data/public/ro/posts/return-your-laptop/attributes.yml +6 -0
  144. data/public/ro/posts/return-your-laptop/body.md +58 -0
  145. data/ro.gemspec +217 -28
  146. data/scripts/speedtest.rb +324 -0
  147. data/tmp/gem-details.oe +0 -0
  148. metadata +214 -74
  149. data/TODO.md +0 -50
  150. data/lib/ro/blankslate.rb +0 -7
  151. data/lib/ro/cache.rb +0 -26
  152. data/lib/ro/git.rb +0 -374
  153. data/lib/ro/initializers/env.rb +0 -5
  154. data/lib/ro/initializers/tilt.rb +0 -104
  155. data/lib/ro/lock.rb +0 -53
  156. data/lib/ro/node/list.rb +0 -142
  157. data/notes/ara.txt +0 -215
data/lib/ro/node.rb CHANGED
@@ -1,478 +1,338 @@
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
22
+ @path.parent.name
31
23
  end
32
24
 
33
- def _type
34
- @type
35
- end
36
-
37
- def path
38
- attributes[:path] || @path
25
+ def identifier
26
+ File.join(type, id)
39
27
  end
40
28
 
41
- def _path
42
- @path
29
+ def inspect
30
+ identifier
43
31
  end
44
32
 
45
- def slug
46
- attributes[:slug] || @slug
33
+ def collection
34
+ @root.collection_for(type)
47
35
  end
48
36
 
49
- def _slug
50
- @slug
37
+ def attributes
38
+ load_attributes
39
+ @attributes
51
40
  end
52
41
 
53
- def identifier
54
- "#{ _type }/#{ _id }"
42
+ def load_attributes
43
+ load_attributes! if @attributes == :lazyload
55
44
  end
56
45
 
57
- def hash
58
- identifier.hash
59
- end
46
+ def load_attributes!
47
+ @attributes = Map.new
60
48
 
61
- def ==(other)
62
- attributes == other.attributes
63
- end
49
+ _load_base_attributes
50
+ _load_file_attributes
51
+ _load_asset_attributes
52
+ _load_meta_attributes
64
53
 
65
- def inspect
66
- identifier
54
+ @attributes
67
55
  end
68
56
 
69
- def to_s
70
- inspect
71
- end
57
+ def _load_base_attributes
58
+ glob = "attributes.{yml,yaml,json}"
72
59
 
73
- def basename
74
- name
60
+ @path.glob(glob) do |file|
61
+ attrs = _render(file)
62
+ update_attributes!(attrs, file:)
63
+ end
75
64
  end
76
65
 
77
- def node
78
- self
79
- end
66
+ def _load_asset_attributes
67
+ {}.tap do |hash|
68
+ assets.each do |asset|
69
+ key = asset.name
70
+ url = asset.url
71
+ path = asset.path.relative_to(@root)
72
+ src = asset.src
73
+ img = asset.img
74
+ size = asset.size
80
75
 
81
- def get(*args)
82
- attributes.get(*args)
83
- end
84
-
85
- def [](*args)
86
- attributes.get(*args)
87
- end
76
+ value = { url:, path:, size:, img:, src: }
88
77
 
89
- def asset_path(*args, &block)
90
- File.join(relative_path, 'assets')
91
- end
78
+ hash[key] = value
79
+ end
92
80
 
93
- def asset_dir
94
- File.join(path, 'assets')
81
+ @attributes.set(assets: hash)
82
+ end
95
83
  end
96
84
 
97
- def asset_paths
98
- Dir.glob("#{ asset_dir }/**/**").select{|entry| test(?f, entry)}
99
- end
85
+ def _load_meta_attributes
86
+ {}.tap do |hash|
87
+ hash.update(
88
+ identifier:,
89
+ type:,
90
+ id:,
91
+ urls:,
92
+ created_at:,
93
+ updated_at:,
94
+ )
100
95
 
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)
96
+ @attributes.set(_meta: hash)
107
97
  end
108
98
  end
109
99
 
110
- def asset_urls
111
- assets.map(&:url)
112
- end
100
+ def _load_file_attributes
101
+ ignored = _ignored_files
113
102
 
114
- def asset_for(*args, &block)
115
- options = Map.options_for!(args)
103
+ @path.files.each do |file|
104
+ next if ignored.include?(file)
116
105
 
117
- path_info = Ro.relative_path_for(args)
106
+ rel = file.relative_to(@path)
118
107
 
119
- path = File.join(@path.to_s, 'assets', path_info)
108
+ key = rel.parts
109
+ basename = key.pop
110
+ base = basename.split('.', 2).first
111
+ key.push(base)
120
112
 
121
- glob = path_info.gsub(/[_-]/, '[_-]')
113
+ value = _render(file)
122
114
 
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 }*")
128
- ]
129
-
130
- candidates = globs.map{|glob| Dir.glob(glob, ::File::FNM_CASEFOLD)}.flatten.compact.uniq
131
-
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
115
+ if value.is_a?(HTML)
116
+ attrs = value.front_matter
117
+ update_attributes!(attrs, file:)
118
+ end
141
119
 
142
- Asset.new(name, :path => path, :url => url)
143
- end
120
+ if @attributes.has?(key)
121
+ raise Error.new("path=#{ @path.inspect } masks #{ key.inspect } in #{ @attributes.inspect }!")
122
+ end
144
123
 
145
- def asset_for?(*args, &block)
146
- begin
147
- asset_for(*args, &block)
148
- rescue
149
- nil
124
+ @attributes.set(key => value)
150
125
  end
151
126
  end
152
127
 
153
- class Asset < ::String
154
- fattr(:path)
155
- fattr(:url)
128
+ def update_attributes!(attrs = {}, **context)
129
+ attrs = Map.for(attrs)
156
130
 
157
- def initialize(name, options = {})
158
- super(name)
159
- ensure
160
- options = Map.for(options)
131
+ blacklist = %w[
132
+ assets
133
+ _meta
134
+ ]
161
135
 
162
- Asset.fattrs.each do |attr|
163
- if options.has_key?(attr)
164
- value = options[attr]
165
- send(attr, value)
166
- end
136
+ blacklist.each do |key|
137
+ if attrs.has_key?(key)
138
+ Ro.error!("#{ key } is blacklisted!", **context)
167
139
  end
168
140
  end
169
141
 
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)
178
- end
142
+ keys = @attributes.depth_first_keys
179
143
 
180
- def extension
181
- base, ext = basename.split('.', 2)
182
- ext
144
+ attrs.depth_first_keys.each do |key|
145
+ if keys.include?(key)
146
+ Ro.error!("#{ attrs.inspect } clobbers #{ @attributes.inspect }!", **context)
147
+ end
183
148
  end
184
- alias_method(:ext, :extension)
185
149
 
186
- def basename
187
- File.basename(path.to_s)
188
- end
150
+ @attributes.update(attrs)
189
151
  end
190
152
 
191
- def url_for(*args, &block)
192
- options = Map.options_for!(args)
193
-
194
- opts = Map.new
153
+ def _ignored_files
154
+ ignored_files =
155
+ %w[
156
+ attributes.yml
157
+ attributes.yaml
158
+ attributes.json
159
+ ./assets/**/**
160
+ ].map do |glob|
161
+ @path.glob(glob).select(&:file?)
162
+ end
195
163
 
196
- opts[:timestamp] = options.delete(:timestamp)
164
+ ignored_files.flatten
165
+ end
197
166
 
198
- args.push(options)
167
+ def _render(file)
168
+ node = self
199
169
 
200
- asset = asset_for(*args, &block)
170
+ value = Ro.render(file, _render_context)
201
171
 
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
207
- end
172
+ if value.is_a?(HTML)
173
+ front_matter = value.front_matter
174
+ html = Ro.expand_asset_urls(value, node)
175
+ value = HTML.new(html, front_matter:)
208
176
  end
209
177
 
210
- if opts.empty?
211
- asset.url
212
- else
213
- query_string = Ro.query_string_for(opts)
214
- "#{ asset.url }?#{ query_string }"
178
+ if value.is_a?(Hash)
179
+ attributes = value
180
+ value = Ro.expand_asset_values(attributes, node)
215
181
  end
182
+
183
+ value
216
184
  end
217
185
 
218
- def url_for?(*args, &block)
219
- begin
220
- url_for(*args, &block)
221
- rescue
222
- nil
186
+ def _render_context
187
+ to_hash.tap do |context|
188
+ context[:ro] ||= root
189
+ context[:collection] ||= collection
223
190
  end
224
191
  end
225
192
 
226
- def source_for(*args)
227
- key = Ro.relative_path_for(:assets, :source, args).split('/')
228
- get(key)
193
+ def fetch(*args)
194
+ attributes.fetch(*args)
229
195
  end
230
196
 
231
- def route(*args)
232
- path_info = Ro.absolute_path_for(Ro.route, relative_path)
233
- [Ro.asset_host, path_info].compact.join('/')
197
+ def get(*args)
198
+ attributes.get(*args)
234
199
  end
235
200
 
236
- def relative_path
237
- re = /^#{ Regexp.escape(Ro.root) }/
238
- @path.to_s.gsub(re, '')
201
+ def [](*args)
202
+ attributes.get(*args)
239
203
  end
240
204
 
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
247
-
248
- if @attributes.has_key?(key)
249
- return @attributes[key]
250
- end
205
+ def relative_path
206
+ path.relative_to(root)
207
+ end
251
208
 
252
- _load do
253
- return(
254
- if @attributes.has_key?(key)
255
- @attributes[key]
256
- else
257
- super
258
- end
259
- )
260
- end
209
+ def asset_dir
210
+ path.join('assets')
261
211
  end
262
212
 
263
- def attributes
264
- _load{ @attributes }
213
+ def asset_paths
214
+ asset_dir.select { |entry| entry.file? }.sort
265
215
  end
266
216
 
267
- def attributes=(attributes)
268
- @attributes = attributes
217
+ def assets
218
+ asset_paths.map { |path| Asset.for(path, node: self) }
269
219
  end
270
-
271
- def instance_eval(*args, &block)
272
- _load{ super }
220
+
221
+ def asset_urls
222
+ assets.map(&:url)
273
223
  end
274
224
 
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)
225
+ def asset_for(*args)
226
+ options = Map.options_for!(args)
281
227
 
282
- related.each do |relationship, value|
283
- unless which.empty?
284
- next unless which.include?(relationship.to_s)
285
- end
228
+ path_info = Path.relative(args)
286
229
 
287
- type, names =
288
- case value
289
- when Hash
290
- value.to_a.first
291
- else
292
- [relationship, value]
293
- end
230
+ path = @path.join('assets', path_info)
294
231
 
295
- names = Coerce.list_of_strings(names)
232
+ glob = path_info.gsub(/[_-]/, '[_-]')
296
233
 
297
- names.each do |name|
298
- identifier = "#{ type }/#{ name }"
299
- node = list.index[identifier]
300
- node._load{ nodes.add(node) }
301
- end
302
- end
234
+ globs =
235
+ [
236
+ @path.call('assets', "#{glob}"),
237
+ @path.call('assets', "#{glob}*"),
238
+ @path.call('assets', "**/#{glob}*")
239
+ ]
303
240
 
304
- case
305
- when block.nil?
306
- nodes
241
+ candidates = globs.map { |glob| Dir.glob(glob, ::File::FNM_CASEFOLD) }.flatten.compact.uniq.sort
307
242
 
308
- when block
309
- nodes.where(&block)
310
- end
311
- }
312
- end
243
+ case candidates.size
244
+ when 0
245
+ raise ArgumentError, "no asset matching #{globs.inspect}"
246
+ else
247
+ path = candidates.last
248
+ end
313
249
 
314
- def reload(&block)
315
- @loaded = false
316
- _load(&block)
250
+ Asset.for(path, node: self)
317
251
  end
318
252
 
319
- def loaded
320
- attributes
253
+ def asset_for?(*args, &block)
254
+ asset_for(*args, &block)
255
+ rescue StandardError
256
+ nil
321
257
  end
322
258
 
323
- def load!(&block)
324
- _load(&block)
259
+ def url_for(relative_path, options = {})
260
+ Ro.url_for(self.relative_path, relative_path, options)
325
261
  end
326
262
 
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
263
+ def path_for(...)
264
+ @path.join(...)
339
265
  end
340
266
 
341
- def _load_from_cache_or_disk
342
- cache_key = _cache_key
343
-
344
- cached = Ro.cache.read(cache_key)
345
-
346
- if cached
347
- Ro.log "loading #{ identifier } from cache"
267
+ def src_for(*args)
268
+ key = Path.relative(:assets, :src, args).split('/')
269
+ get(key)
270
+ end
348
271
 
349
- @attributes = Map.new.update(cached)
272
+ def method_missing(method, *args, &block)
273
+ key = method.to_s
350
274
 
351
- return :cache
275
+ if attributes.has_key?(key)
276
+ attributes[key]
352
277
  else
353
- Ro.log "loading #{ identifier } from disk"
354
-
355
- @attributes = Map.new
356
-
357
- _load_attributes_yml
358
- _load_attribute_files
359
- _load_sources
360
- _load_assets
361
-
362
- Ro.cache.write(cache_key, @attributes)
363
-
364
- return :disk
278
+ super
365
279
  end
366
280
  end
367
281
 
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
-
380
- @attributes
381
- end
282
+ def to_hash
283
+ attributes.to_hash
382
284
  end
383
285
 
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\//
396
-
397
- key = relative_path.split('.', 2).first.split('/')
398
-
399
- html = Ro.render(path, node)
400
- html = Ro.expand_asset_urls(html, node)
401
-
402
- @attributes.set(key => html)
403
- end
286
+ def to_s(...)
287
+ to_json(...)
404
288
  end
405
289
 
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)
415
-
416
- next if basename == 'attributes.yml'
417
-
418
- value = Ro.render_source(path, node)
419
- @attributes.set([:assets, :source, basename] => value)
420
- end
290
+ def to_str(...)
291
+ to_json(...)
421
292
  end
422
293
 
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')
436
-
437
- @attributes.set(key => url)
438
- end
294
+ def to_json(...)
295
+ JSON.pretty_generate(to_hash, ...)
439
296
  end
440
297
 
441
- def _binding
442
- Kernel.binding
298
+ def as_json(...)
299
+ to_hash.as_json(...)
443
300
  end
444
301
 
445
- def _cache_key
446
- glob = File.join(@path, '**/**')
302
+ def to_yaml(...)
303
+ to_hash.to_yaml(...)
304
+ end
447
305
 
448
- entries = []
306
+ def files
307
+ path.glob('**/**').select { |entry| entry.file? }.sort
308
+ end
449
309
 
450
- Dir.glob(glob) do |entry|
451
- stat =
452
- begin
453
- File.stat(entry)
454
- rescue
455
- next
456
- end
310
+ def urls
311
+ files.map { |file| url_for(file.relative_to(@path)) }.sort
312
+ end
457
313
 
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
314
+ def <=>(other)
315
+ sort_key <=> other.sort_key
316
+ end
462
317
 
463
- fingerprint = entries.map{|pair| pair.join('@')}.join(', ')
318
+ def sort_key
319
+ default_sort_key
320
+ end
464
321
 
465
- md5 = Ro.md5(fingerprint)
322
+ def default_sort_key
323
+ position = (attributes[:position] ? Float(attributes[:position]) : 0.0)
324
+ published_at = (attributes[:published_at] ? Time.parse(attributes[:published_at].to_s) : Time.at(0)).utc.iso8601
325
+ created_at = (attributes[:created_at] ? Time.parse(attributes[:created_at].to_s) : Time.at(0)).utc.iso8601
466
326
 
467
- [@path, md5]
327
+ [position, published_at, created_at, name]
468
328
  end
469
329
 
470
- def _load_from_cache
471
- false
330
+ def created_at
331
+ files.map{|file| File.stat(file).ctime}.min
472
332
  end
473
333
 
474
- def _attributes_yml
475
- File.join(@path, 'attributes.yml')
334
+ def updated_at
335
+ files.map{|file| File.stat(file).mtime}.max
476
336
  end
477
337
  end
478
338
  end