ro 4.2.0 → 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 (156) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +29 -8
  3. data/LICENSE +1 -1
  4. data/README.md +274 -111
  5. data/Rakefile +2 -2
  6. data/lib/ro/_lib.rb +18 -6
  7. data/lib/ro/asset.rb +23 -13
  8. data/lib/ro/collection.rb +47 -4
  9. data/lib/ro/config.rb +4 -0
  10. data/lib/ro/error.rb +5 -2
  11. data/lib/ro/html.rb +23 -0
  12. data/lib/ro/html_safe.rb +143 -0
  13. data/lib/ro/methods.rb +95 -38
  14. data/lib/ro/node.rb +78 -35
  15. data/lib/ro/path.rb +4 -0
  16. data/lib/ro/template.rb +62 -22
  17. data/lib/ro/text.rb +120 -0
  18. data/lib/ro.rb +4 -0
  19. data/public/api/ro/index-1.json +997 -79
  20. data/public/api/ro/index.json +997 -79
  21. data/public/api/ro/nerd/fastest-possible-embeddings/index.json +90 -0
  22. data/public/api/ro/nerd/ima/index.json +49 -0
  23. data/public/api/ro/nerd/index/index.json +74 -0
  24. data/public/api/ro/nerd/index-1.json +204 -0
  25. data/public/api/ro/nerd/index.json +194 -0
  26. data/public/api/ro/pages/about/index.json +60 -0
  27. data/public/api/ro/pages/contact/index.json +50 -0
  28. data/public/api/ro/pages/cv/index.json +49 -0
  29. data/public/api/ro/pages/disco/index.json +117 -0
  30. data/public/api/ro/pages/index/index.json +30 -0
  31. data/public/api/ro/pages/index-1.json +366 -0
  32. data/public/api/ro/pages/index.json +356 -0
  33. data/public/api/ro/pages/jess/index.json +62 -0
  34. data/public/api/ro/pages/now/index.json +43 -0
  35. data/public/api/ro/posts/almost-died-in-an-ice-cave/index.json +265 -0
  36. data/public/api/ro/posts/facebook-and-global-extremism/index.json +90 -0
  37. data/public/api/ro/posts/index-1.json +461 -79
  38. data/public/api/ro/posts/index.json +461 -79
  39. data/public/api/ro/posts/lemmings-considered-harmful/index.json +49 -0
  40. data/public/api/ro/posts/lost-in-the-desert/index.json +49 -0
  41. data/public/api/ro/posts/mission/index.json +49 -0
  42. data/public/api/ro/posts/return-your-laptop/index.json +61 -0
  43. data/public/ro/nerd/fastest-possible-embeddings/assets/giraffe.jpeg +0 -0
  44. data/public/ro/nerd/fastest-possible-embeddings/assets/let-me-in.jpg +0 -0
  45. data/public/ro/nerd/fastest-possible-embeddings/assets/src/fastembed.js +70 -0
  46. data/public/ro/nerd/fastest-possible-embeddings/assets/src/fastembed.rs +68 -0
  47. data/public/ro/nerd/fastest-possible-embeddings/assets/terminal.jpg +0 -0
  48. data/public/ro/nerd/fastest-possible-embeddings/attributes.yml +7 -0
  49. data/public/ro/nerd/fastest-possible-embeddings/body.md +266 -0
  50. data/public/ro/nerd/ima/assets/og.jpeg +0 -0
  51. data/public/ro/nerd/ima/attributes.yml +8 -0
  52. data/public/ro/nerd/ima/body.md +22 -0
  53. data/public/ro/nerd/index/assets/giraffe.jpeg +0 -0
  54. data/public/ro/nerd/index/assets/let-me-in.jpg +0 -0
  55. data/public/ro/nerd/index/assets/terminal.jpg +0 -0
  56. data/public/ro/nerd/index/attributes.yml +7 -0
  57. data/public/ro/nerd/index/body.md +130 -0
  58. data/public/ro/pages/about/assets/og.jpeg +0 -0
  59. data/public/ro/pages/about/assets/speak-english-pulp-fiction.gif +0 -0
  60. data/public/ro/pages/about/body.md +40 -0
  61. data/public/ro/pages/contact/assets/giraffe.jpeg +0 -0
  62. data/public/ro/pages/contact/attributes.yml +7 -0
  63. data/public/ro/pages/contact/body.md +9 -0
  64. data/public/ro/pages/cv/assets/ara.jpg +0 -0
  65. data/public/ro/pages/cv/attributes.yml +6 -0
  66. data/public/ro/pages/cv/body.md +122 -0
  67. data/public/ro/pages/disco/assets/disco.jpg +0 -0
  68. data/public/ro/pages/disco/assets/disco.png +0 -0
  69. data/public/ro/pages/disco/assets/speak-english-pulp-fiction.gif +0 -0
  70. data/public/ro/pages/disco/assets/src/environment.md +2354 -0
  71. data/public/ro/pages/disco/assets/src/fortune-500.md +2518 -0
  72. data/public/ro/pages/disco/assets/src/greed.md +2703 -0
  73. data/public/ro/pages/disco/assets/src/up-at-night.md +2337 -0
  74. data/public/ro/pages/disco/attributes.yml +9 -0
  75. data/public/ro/pages/disco/body.md +99 -0
  76. data/public/ro/pages/disco/samples/environment.md +2354 -0
  77. data/public/ro/pages/disco/samples/fortune-500.md +2518 -0
  78. data/public/ro/pages/disco/samples/greed.md +2703 -0
  79. data/public/ro/pages/disco/samples/up-at-night.md +2337 -0
  80. data/public/ro/pages/index/attributes.yml +1 -0
  81. data/public/ro/pages/index/body.md +15 -0
  82. data/public/ro/pages/jess/assets/og.jpg +0 -0
  83. data/public/ro/pages/jess/assets/speak-english-pulp-fiction.gif +0 -0
  84. data/public/ro/pages/jess/attributes.yml +7 -0
  85. data/public/ro/pages/jess/body.md +3 -0
  86. data/public/ro/pages/now/assets/speak-english-pulp-fiction.gif +0 -0
  87. data/public/ro/pages/now/attributes.yml +1 -0
  88. data/public/ro/pages/now/body.md +24 -0
  89. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image1.png +0 -0
  90. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image10.png +0 -0
  91. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image11.png +0 -0
  92. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image12.png +0 -0
  93. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image13.png +0 -0
  94. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image14.png +0 -0
  95. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image15.png +0 -0
  96. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image2.png +0 -0
  97. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image3.png +0 -0
  98. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image4.png +0 -0
  99. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image5.png +0 -0
  100. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image6.png +0 -0
  101. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image7.png +0 -0
  102. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image8.png +0 -0
  103. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image9.png +0 -0
  104. data/public/ro/posts/almost-died-in-an-ice-cave/assets/josh-pointing.jpg +0 -0
  105. data/public/ro/posts/almost-died-in-an-ice-cave/assets/levi-rawr.png +0 -0
  106. data/public/ro/posts/almost-died-in-an-ice-cave/assets/og.jpg +0 -0
  107. data/public/ro/posts/almost-died-in-an-ice-cave/assets/purple-heart.jpg +0 -0
  108. data/public/ro/posts/almost-died-in-an-ice-cave/attributes.yml +6 -0
  109. data/public/ro/posts/almost-died-in-an-ice-cave/body.md +419 -0
  110. data/public/ro/posts/facebook-and-global-extremism/assets/background.html +125 -0
  111. data/public/ro/posts/facebook-and-global-extremism/assets/background.md +95 -0
  112. data/public/ro/posts/facebook-and-global-extremism/assets/og.jpg +0 -0
  113. data/public/ro/posts/facebook-and-global-extremism/assets/prompt.txt +122 -0
  114. data/public/ro/posts/facebook-and-global-extremism/assets/results.md +183 -0
  115. data/public/ro/posts/facebook-and-global-extremism/assets/survey.txt +190 -0
  116. data/public/ro/posts/facebook-and-global-extremism/attributes.yml +7 -0
  117. data/public/ro/posts/facebook-and-global-extremism/body.md +393 -0
  118. data/public/ro/posts/lemmings-considered-harmful/assets/lemming.jpeg +0 -0
  119. data/public/ro/posts/lemmings-considered-harmful/attributes.yml +6 -0
  120. data/public/ro/posts/lemmings-considered-harmful/body.md +43 -0
  121. data/public/ro/posts/lost-in-the-desert/assets/og.jpg +0 -0
  122. data/public/ro/posts/lost-in-the-desert/attributes.yml +6 -0
  123. data/public/ro/posts/lost-in-the-desert/body.md +7 -0
  124. data/public/ro/posts/mission/assets/og.jpg +0 -0
  125. data/public/ro/posts/mission/attributes.yml +6 -0
  126. data/public/ro/posts/mission/body.md +4 -0
  127. data/public/ro/posts/return-your-laptop/assets/og.jpg +0 -0
  128. data/public/ro/posts/return-your-laptop/assets/return-your-laptop.png +0 -0
  129. data/public/ro/posts/return-your-laptop/attributes.yml +6 -0
  130. data/public/ro/posts/return-your-laptop/body.md +58 -0
  131. data/ro.gemspec +178 -49
  132. data/scripts/speedtest.rb +324 -0
  133. data/tmp/gem-details.oe +0 -0
  134. metadata +157 -33
  135. data/public/api/ro/posts/first_post/index.json +0 -52
  136. data/public/api/ro/posts/second_post/index.json +0 -51
  137. data/public/api/ro/posts/third_post/index.json +0 -51
  138. data/public/ro/posts/first_post/assets/foo/bar/baz.jpg +0 -0
  139. data/public/ro/posts/first_post/assets/foo.jpg +0 -0
  140. data/public/ro/posts/first_post/assets/src/foo/bar.rb +0 -3
  141. data/public/ro/posts/first_post/attributes.yml +0 -2
  142. data/public/ro/posts/first_post/blurb.erb.md +0 -7
  143. data/public/ro/posts/first_post/body.md +0 -16
  144. data/public/ro/posts/first_post/testing.txt +0 -3
  145. data/public/ro/posts/second_post/assets/foo/bar/baz.jpg +0 -0
  146. data/public/ro/posts/second_post/assets/foo.jpg +0 -0
  147. data/public/ro/posts/second_post/assets/src/foo/bar.rb +0 -3
  148. data/public/ro/posts/second_post/attributes.yml +0 -2
  149. data/public/ro/posts/second_post/blurb.erb.md +0 -5
  150. data/public/ro/posts/second_post/body.md +0 -16
  151. data/public/ro/posts/third_post/assets/foo/bar/baz.jpg +0 -0
  152. data/public/ro/posts/third_post/assets/foo.jpg +0 -0
  153. data/public/ro/posts/third_post/assets/src/foo/bar.rb +0 -3
  154. data/public/ro/posts/third_post/attributes.yml +0 -2
  155. data/public/ro/posts/third_post/blurb.erb.md +0 -5
  156. data/public/ro/posts/third_post/body.md +0 -16
data/lib/ro/collection.rb CHANGED
@@ -42,13 +42,56 @@ module Ro
42
42
  @path.subdirectory_for(name)
43
43
  end
44
44
 
45
- def each(&block)
45
+ def each(offset:nil, limit:nil, &block)
46
46
  accum = []
47
47
 
48
- subdirectories do |subdirectory|
49
- node = node_for(subdirectory)
48
+ if offset
49
+ i = -1
50
+ n = 0
51
+ subdirectories do |subdirectory|
52
+ i += 1
53
+ next if i < offset
54
+ node = node_for(subdirectory)
55
+ block ? block.call(node) : accum.push(node)
56
+ n += 1
57
+ break if limit && n >= limit
58
+ end
59
+ else
60
+ subdirectories do |subdirectory|
61
+ node = node_for(subdirectory)
62
+ block ? block.call(node) : accum.push(node)
63
+ end
64
+ end
65
+
66
+ block ? self : accum
67
+ end
68
+
69
+ class Page < ::Array
70
+ attr_accessor :number
71
+
72
+ def initialize(nodes = [], number: 1)
73
+ replace(nodes)
74
+ @number = number
75
+ end
76
+ end
77
+
78
+ def page(number, size: 10)
79
+ offset = [(number - 1), 0].max * size
80
+ limit = [size, 1].max
81
+
82
+ nodes = each(offset:, limit:)
83
+ Page.new(nodes, number:)
84
+ end
85
+
86
+ def paginate(size: 10, &block)
87
+ number = 0
88
+ accum = []
50
89
 
51
- block ? block.call(node) : accum.push(node)
90
+ loop do
91
+ number += 1
92
+ page = self.page(number, size:)
93
+ break if page.empty?
94
+ block ? block.call(page) : accum.push(page)
52
95
  end
53
96
 
54
97
  block ? self : accum
data/lib/ro/config.rb CHANGED
@@ -11,6 +11,9 @@ module Ro
11
11
  :url =>
12
12
  (Ro.env.url || Ro.defaults.url),
13
13
 
14
+ :img_url =>
15
+ (Ro.env.img_url || Ro.defaults.img_url),
16
+
14
17
  :page_size =>
15
18
  (Ro.env.page_size || Ro.defaults.page_size),
16
19
 
@@ -48,6 +51,7 @@ module Ro
48
51
  :root => :root,
49
52
  :build => :path,
50
53
  :url => :url,
54
+ :img_url => :url,
51
55
  :page_size => :int,
52
56
  :log => :bool,
53
57
  :debug => :bool,
data/lib/ro/error.rb CHANGED
@@ -1,8 +1,11 @@
1
1
  module Ro
2
2
  class Error < ::StandardError
3
- def initialize(message, context = nil)
4
- super(message)
3
+ attr_reader :context
4
+
5
+ def initialize(message, **context)
5
6
  @context = context
7
+ msg = context.empty? ? "#{ message }" : "#{ message }, #{ context.inspect }"
8
+ super(msg)
6
9
  end
7
10
  end
8
11
  end
data/lib/ro/html.rb ADDED
@@ -0,0 +1,23 @@
1
+ module Ro
2
+ require_relative 'html_safe'
3
+
4
+ class HTML < ::ActiveSupport::SafeBuffer
5
+ def initialize(*args, **kws, &block)
6
+ self.front_matter = kws.fetch(:front_matter){ {} }
7
+
8
+ super(args.join)
9
+ end
10
+
11
+ def front_matter
12
+ @front_matter ||= Map.new
13
+ end
14
+
15
+ def front_matter=(hash = {})
16
+ @front_matter = Map.for(hash)
17
+ end
18
+
19
+ def attributes
20
+ front_matter
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ unless defined?(ActiveSupport::SafeBuffer)
4
+
5
+ class Object
6
+ def html_safe?
7
+ false
8
+ end
9
+ end
10
+
11
+ class Numeric
12
+ def html_safe?
13
+ true
14
+ end
15
+ end
16
+
17
+ module ActiveSupport #:nodoc:
18
+ class SafeBuffer < String
19
+ UNSAFE_STRING_METHODS = %w(
20
+ capitalize chomp chop delete downcase gsub lstrip next reverse rstrip
21
+ slice squeeze strip sub succ swapcase tr tr_s upcase
22
+ )
23
+
24
+ alias_method :original_concat, :concat
25
+ private :original_concat
26
+
27
+ # Raised when <tt>ActiveSupport::SafeBuffer#safe_concat</tt> is called on unsafe buffers.
28
+ class SafeConcatError < StandardError
29
+ def initialize
30
+ super "Could not concatenate to the buffer because it is not html safe."
31
+ end
32
+ end
33
+
34
+ def [](*args)
35
+ if args.size < 2
36
+ super
37
+ elsif html_safe?
38
+ new_safe_buffer = super
39
+
40
+ if new_safe_buffer
41
+ new_safe_buffer.instance_variable_set :@html_safe, true
42
+ end
43
+
44
+ new_safe_buffer
45
+ else
46
+ to_str[*args]
47
+ end
48
+ end
49
+
50
+ def safe_concat(value)
51
+ raise SafeConcatError unless html_safe?
52
+ original_concat(value)
53
+ end
54
+
55
+ def initialize(str = "")
56
+ @html_safe = true
57
+ super
58
+ end
59
+
60
+ def initialize_copy(other)
61
+ super
62
+ @html_safe = other.html_safe?
63
+ end
64
+
65
+ def clone_empty
66
+ self[0, 0]
67
+ end
68
+
69
+ def concat(value)
70
+ super(html_escape_interpolated_argument(value))
71
+ end
72
+ alias << concat
73
+
74
+ def prepend(value)
75
+ super(html_escape_interpolated_argument(value))
76
+ end
77
+
78
+ def +(other)
79
+ dup.concat(other)
80
+ end
81
+
82
+ def %(args)
83
+ case args
84
+ when Hash
85
+ escaped_args = Hash[args.map { |k, arg| [k, html_escape_interpolated_argument(arg)] }]
86
+ else
87
+ escaped_args = Array(args).map { |arg| html_escape_interpolated_argument(arg) }
88
+ end
89
+
90
+ self.class.new(super(escaped_args))
91
+ end
92
+
93
+ def html_safe?
94
+ defined?(@html_safe) && @html_safe
95
+ end
96
+
97
+ def to_s
98
+ self
99
+ end
100
+
101
+ def to_param
102
+ to_str
103
+ end
104
+
105
+ def encode_with(coder)
106
+ coder.represent_object nil, to_str
107
+ end
108
+
109
+ UNSAFE_STRING_METHODS.each do |unsafe_method|
110
+ if unsafe_method.respond_to?(unsafe_method)
111
+ class_eval <<-EOT, __FILE__, __LINE__ + 1
112
+ def #{unsafe_method}(*args, &block) # def capitalize(*args, &block)
113
+ to_str.#{unsafe_method}(*args, &block) # to_str.capitalize(*args, &block)
114
+ end # end
115
+
116
+ def #{unsafe_method}!(*args) # def capitalize!(*args)
117
+ @html_safe = false # @html_safe = false
118
+ super # super
119
+ end # end
120
+ EOT
121
+ end
122
+ end
123
+
124
+ private
125
+
126
+ def html_escape_interpolated_argument(arg)
127
+ (!html_safe? || arg.html_safe?) ? arg : CGI.escapeHTML(arg.to_s)
128
+ end
129
+ end
130
+ end
131
+
132
+ class String
133
+ # Marks a string as trusted safe. It will be inserted into HTML with no
134
+ # additional escaping performed. It is your responsibility to ensure that the
135
+ # string contains no malicious content. This method is equivalent to the
136
+ # +raw+ helper in views. It is recommended that you use +sanitize+ instead of
137
+ # this method. It should never be called on user input.
138
+ def html_safe
139
+ ActiveSupport::SafeBuffer.new(self)
140
+ end
141
+ end
142
+
143
+ end
data/lib/ro/methods.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module Ro
2
- module Methods
2
+ module Methods
3
3
  # cast methods
4
4
  # |
5
5
  # v
@@ -51,10 +51,19 @@ module Ro
51
51
  # v
52
52
  def url_for(path, *args)
53
53
  options = Map.extract_options!(args)
54
- base = options[:base] || options[:url] || Ro.config.url
54
+
55
+ base = (options.delete(:base) || options.delete(:url))
55
56
 
56
57
  path = Path.for(path, *args)
57
58
 
59
+ base ||= (
60
+ if Ro.is_image?(path)
61
+ Ro.config.img_url
62
+ else
63
+ Ro.config.url
64
+ end
65
+ )
66
+
58
67
  fragment = options.delete(:fragment)
59
68
  query = options.delete(:query) || options
60
69
 
@@ -63,8 +72,7 @@ module Ro
63
72
  uri.path = '' if uri.path == '/'
64
73
 
65
74
  uri.query = query_string_for(query) unless query.empty?
66
-
67
- uri.fragment = fragment if fragment
75
+ uri.fragment = fragment unless fragment.nil?
68
76
 
69
77
  uri.to_s
70
78
  end
@@ -127,8 +135,8 @@ module Ro
127
135
  end
128
136
  end
129
137
 
130
- def error!(message, context = nil)
131
- error = Error.new(message, context)
138
+ def error!(message, **context)
139
+ error = Error.new(message, **context)
132
140
 
133
141
  begin
134
142
  raise error
@@ -139,6 +147,14 @@ module Ro
139
147
  end
140
148
  end
141
149
 
150
+ def emsg(e)
151
+ if e.is_a?(Exception)
152
+ "#{ e.message } (#{ e.class.name })\n#{ Array(e.backtrace).join(10.chr) }"
153
+ else
154
+ e.to_s
155
+ end
156
+ end
157
+
142
158
  # template methods
143
159
  # |
144
160
  # v
@@ -158,7 +174,8 @@ module Ro
158
174
  # |
159
175
  # v
160
176
  EXPAND_ASSET_URL_STRATEGIES = %i[
161
- accurate_expand_asset_urls sloppy_expand_asset_urls
177
+ accurate_expand_asset_urls
178
+ sloppy_expand_asset_urls
162
179
  ]
163
180
 
164
181
  def expand_asset_url_strategies
@@ -166,55 +183,58 @@ module Ro
166
183
  end
167
184
 
168
185
  def expand_asset_urls(html, node)
169
- last = expand_asset_url_strategies.size - 1
186
+ strategies = expand_asset_url_strategies
187
+ error = nil
170
188
 
171
- expand_asset_url_strategies.each_with_index do |strategy, i|
189
+ strategies.each do |strategy|
172
190
  return send(strategy, html, node)
173
191
  rescue Object => e
174
- raise if i == last
175
-
176
- Ro.log(e)
192
+ error = e
193
+ Ro.log(:error, emsg(error))
194
+ Ro.log(:error, "failed to expand assets via #{ strategy }")
177
195
  end
178
196
 
179
- Ro.error! "could not expand assets via #{expand_asset_url_strategies.join(' | ')}"
197
+ raise error
180
198
  end
181
199
 
182
200
  def accurate_expand_asset_urls(html, node)
183
- doc = REXML::Document.new('<__ro__>' + html + '</__ro__>')
201
+ doc = Nokogiri::HTML.fragment(html.to_str)
184
202
 
185
- doc.each_recursive do |element|
186
- next unless element.respond_to?(:attributes)
187
-
188
- src = {}
189
- element.attributes.each do |key, value|
190
- src[key] = value
191
- end
192
-
193
- dst = expand_asset_values(src, node)
194
-
195
- dst.each do |k, v|
196
- element.attributes[k] = v
203
+ doc.traverse do |element|
204
+ if element.respond_to?(:attributes)
205
+ attributes = element.attributes
206
+ expand_asset_values!(attributes) unless attributes.empty?
197
207
  end
198
208
  end
199
209
 
200
- doc.to_s.tap do |xml|
201
- xml.sub!(/^\s*<.?__ro__>\s*/, '')
202
- xml.sub!(/\s*<.?__ro__>\s*$/, '')
203
- xml.strip!
204
- end
210
+ expanded = doc.to_s.strip
211
+
212
+ HTML.new(expanded)
205
213
  end
206
214
 
207
215
  def sloppy_expand_asset_urls(html, node)
208
- html.to_s.gsub(%r{\s*=\s*['"](?:[.]/)?assets/[^'"\s]+['"]}) do |match|
209
- path = match[%r{assets/[^'"\s]+}]
210
- url = node.url_for(path)
211
- "='#{url}'"
212
- end
216
+ re = %r`\s*=\s*['"](?:[.]/)?(assets/[^'"\s]+)['"]`
217
+
218
+ expanded =
219
+ html.to_str.gsub(re) do |match|
220
+ path = match[%r`assets/[^'"\s]+`]
221
+
222
+ if node.path_for(path).exist?
223
+ url = node.url_for(path)
224
+ "='#{url}'"
225
+ else
226
+ match
227
+ end
228
+ end
229
+
230
+ HTML.new(expanded)
213
231
  end
214
232
 
215
- def expand_asset_values(hash, node)
233
+ def expand_asset_values(hash, node)
216
234
  src = Map.for(hash)
217
- dst = Map.new
235
+ dst = Map.new(hash)
236
+
237
+ return dst if src.empty?
218
238
 
219
239
  re = %r{\A(?:[.]/)?(assets/[^\s]+)\s*\z}
220
240
 
@@ -232,6 +252,43 @@ module Ro
232
252
 
233
253
  dst.to_hash
234
254
  end
255
+
256
+ def expand_asset_values!(hash, node)
257
+ expand_asset_values(hash, node).each do |key, value|
258
+ hash[key] = value
259
+ end
260
+ end
261
+
262
+ #
263
+ DEFAULT_IMAGE_EXTENSIONS = %i[
264
+ webp jpg jpeg png gif tif tiff svg
265
+ ]
266
+
267
+ DEFAULT_IMAGE_PATTERNS = [
268
+ /[.](#{ DEFAULT_IMAGE_EXTENSIONS.join('|') })$/i
269
+ ]
270
+
271
+ def image_patterns
272
+ @image_patterns ||= DEFAULT_IMAGE_PATTERNS.dup
273
+ end
274
+
275
+ def image_pattern
276
+ Regexp.union(Ro.image_patterns)
277
+ end
278
+
279
+ def is_image?(path)
280
+ !!(URI.parse(path.to_s).path =~ Ro.image_pattern)
281
+ end
282
+
283
+ def image_info(path)
284
+ is = ImageSize.path(path)
285
+ format, width, height = is.format.to_s, is.width, is.height
286
+ {format:, width:, height:}
287
+ end
288
+
289
+ def uuid
290
+ SecureRandom.uuid_v7.to_s
291
+ end
235
292
  end
236
293
 
237
294
  extend Methods
data/lib/ro/node.rb CHANGED
@@ -47,40 +47,34 @@ module Ro
47
47
  @attributes = Map.new
48
48
 
49
49
  _load_base_attributes
50
+ _load_file_attributes
50
51
  _load_asset_attributes
51
52
  _load_meta_attributes
52
- _load_file_attributes
53
53
 
54
54
  @attributes
55
55
  end
56
56
 
57
57
  def _load_base_attributes
58
- disallowed =
59
- %w[
60
- assets
61
- _meta
62
- ]
63
-
64
- glob =
65
- "attributes.{yml,yaml,json}"
58
+ glob = "attributes.{yml,yaml,json}"
66
59
 
67
60
  @path.glob(glob) do |file|
68
61
  attrs = _render(file)
69
-
70
- disallowed.each do |key|
71
- Ro.error!("#{ file } must not contain the key #{key.inspect}") if attrs.has_key?(key)
72
- end
73
-
74
- @attributes.update(attrs)
62
+ update_attributes!(attrs, file:)
75
63
  end
76
64
  end
77
65
 
78
-
79
66
  def _load_asset_attributes
80
67
  {}.tap do |hash|
81
68
  assets.each do |asset|
82
69
  key = asset.name
83
- value = { url: asset.url, path: asset.path.relative_to(@root), src: asset.src }
70
+ url = asset.url
71
+ path = asset.path.relative_to(@root)
72
+ src = asset.src
73
+ img = asset.img
74
+ size = asset.size
75
+
76
+ value = { url:, path:, size:, img:, src: }
77
+
84
78
  hash[key] = value
85
79
  end
86
80
 
@@ -94,7 +88,9 @@ module Ro
94
88
  identifier:,
95
89
  type:,
96
90
  id:,
97
- urls:
91
+ urls:,
92
+ created_at:,
93
+ updated_at:,
98
94
  )
99
95
 
100
96
  @attributes.set(_meta: hash)
@@ -114,16 +110,46 @@ module Ro
114
110
  base = basename.split('.', 2).first
115
111
  key.push(base)
116
112
 
117
- if @attributes.has?(key)
118
- raise Error.new("#{ @path } clobbers #{ key.inspect }!")
113
+ value = _render(file)
114
+
115
+ if value.is_a?(HTML)
116
+ attrs = value.front_matter
117
+ update_attributes!(attrs, file:)
119
118
  end
120
119
 
121
- value = _render(file)
120
+ if @attributes.has?(key)
121
+ raise Error.new("path=#{ @path.inspect } masks #{ key.inspect } in #{ @attributes.inspect }!")
122
+ end
122
123
 
123
124
  @attributes.set(key => value)
124
125
  end
125
126
  end
126
127
 
128
+ def update_attributes!(attrs = {}, **context)
129
+ attrs = Map.for(attrs)
130
+
131
+ blacklist = %w[
132
+ assets
133
+ _meta
134
+ ]
135
+
136
+ blacklist.each do |key|
137
+ if attrs.has_key?(key)
138
+ Ro.error!("#{ key } is blacklisted!", **context)
139
+ end
140
+ end
141
+
142
+ keys = @attributes.depth_first_keys
143
+
144
+ attrs.depth_first_keys.each do |key|
145
+ if keys.include?(key)
146
+ Ro.error!("#{ attrs.inspect } clobbers #{ @attributes.inspect }!", **context)
147
+ end
148
+ end
149
+
150
+ @attributes.update(attrs)
151
+ end
152
+
127
153
  def _ignored_files
128
154
  ignored_files =
129
155
  %w[
@@ -139,11 +165,19 @@ module Ro
139
165
  end
140
166
 
141
167
  def _render(file)
168
+ node = self
169
+
142
170
  value = Ro.render(file, _render_context)
143
171
 
144
- if value.is_a?(Ro::Template::HTML)
145
- html = value
146
- value = Ro.expand_asset_urls(html, self)
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:)
176
+ end
177
+
178
+ if value.is_a?(Hash)
179
+ attributes = value
180
+ value = Ro.expand_asset_values(attributes, node)
147
181
  end
148
182
 
149
183
  value
@@ -156,6 +190,10 @@ module Ro
156
190
  end
157
191
  end
158
192
 
193
+ def fetch(*args)
194
+ attributes.fetch(*args)
195
+ end
196
+
159
197
  def get(*args)
160
198
  attributes.get(*args)
161
199
  end
@@ -219,14 +257,13 @@ module Ro
219
257
  end
220
258
 
221
259
  def url_for(relative_path, options = {})
222
- raise ArgumentError, relative_path if Path.absolute?(relative_path)
223
-
224
- fullpath = Path.for(path, relative_path).expand
225
- raise ArgumentError, "#{relative_path.inspect} -- DOES NOT EXIST" unless fullpath.exist?
226
-
227
260
  Ro.url_for(self.relative_path, relative_path, options)
228
261
  end
229
262
 
263
+ def path_for(...)
264
+ @path.join(...)
265
+ end
266
+
230
267
  def src_for(*args)
231
268
  key = Path.relative(:assets, :src, args).split('/')
232
269
  get(key)
@@ -250,6 +287,10 @@ module Ro
250
287
  to_json(...)
251
288
  end
252
289
 
290
+ def to_str(...)
291
+ to_json(...)
292
+ end
293
+
253
294
  def to_json(...)
254
295
  JSON.pretty_generate(to_hash, ...)
255
296
  end
@@ -262,12 +303,6 @@ module Ro
262
303
  to_hash.to_yaml(...)
263
304
  end
264
305
 
265
- def _mapify(data)
266
- converted = 'this_recursively_converts_nested_hashes_into_maps'
267
-
268
- Map.for(converted => data)[converted]
269
- end
270
-
271
306
  def files
272
307
  path.glob('**/**').select { |entry| entry.file? }.sort
273
308
  end
@@ -291,5 +326,13 @@ module Ro
291
326
 
292
327
  [position, published_at, created_at, name]
293
328
  end
329
+
330
+ def created_at
331
+ files.map{|file| File.stat(file).ctime}.min
332
+ end
333
+
334
+ def updated_at
335
+ files.map{|file| File.stat(file).mtime}.max
336
+ end
294
337
  end
295
338
  end
data/lib/ro/path.rb CHANGED
@@ -221,5 +221,9 @@ module Ro
221
221
  def <=>(other)
222
222
  sort_key <=> other.sort_key
223
223
  end
224
+
225
+ def stat
226
+ File.stat(self)
227
+ end
224
228
  end
225
229
  end