ro 4.2.0 → 5.0.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.
- checksums.yaml +4 -4
- data/Gemfile.lock +57 -10
- data/LICENSE +1 -1
- data/MIGRATION.md +320 -0
- data/README.md +286 -111
- data/Rakefile +2 -2
- data/a.yml +60 -0
- data/bin/ro +10 -0
- data/lib/ro/_lib.rb +18 -6
- data/lib/ro/asset.rb +67 -16
- data/lib/ro/collection.rb +91 -10
- data/lib/ro/config.rb +4 -0
- data/lib/ro/error.rb +5 -2
- data/lib/ro/html.rb +23 -0
- data/lib/ro/html_safe.rb +143 -0
- data/lib/ro/methods.rb +95 -38
- data/lib/ro/migrator.rb +285 -0
- data/lib/ro/node.rb +128 -45
- data/lib/ro/path.rb +4 -0
- data/lib/ro/root.rb +75 -1
- data/lib/ro/script/migrate.rb +204 -0
- data/lib/ro/script/server.rb +1 -1
- data/lib/ro/template.rb +62 -22
- data/lib/ro/text.rb +120 -0
- data/lib/ro.rb +5 -0
- data/public/api/ro/index-1.json +997 -79
- data/public/api/ro/index.json +997 -79
- data/public/api/ro/nerd/fastest-possible-embeddings/index.json +90 -0
- data/public/api/ro/nerd/ima/index.json +49 -0
- data/public/api/ro/nerd/index/index.json +74 -0
- data/public/api/ro/nerd/index-1.json +204 -0
- data/public/api/ro/nerd/index.json +194 -0
- data/public/api/ro/pages/about/index.json +60 -0
- data/public/api/ro/pages/contact/index.json +50 -0
- data/public/api/ro/pages/cv/index.json +49 -0
- data/public/api/ro/pages/disco/index.json +117 -0
- data/public/api/ro/pages/index/index.json +30 -0
- data/public/api/ro/pages/index-1.json +366 -0
- data/public/api/ro/pages/index.json +356 -0
- data/public/api/ro/pages/jess/index.json +62 -0
- data/public/api/ro/pages/now/index.json +43 -0
- data/public/api/ro/posts/almost-died-in-an-ice-cave/index.json +265 -0
- data/public/api/ro/posts/facebook-and-global-extremism/index.json +90 -0
- data/public/api/ro/posts/index-1.json +461 -79
- data/public/api/ro/posts/index.json +461 -79
- data/public/api/ro/posts/lemmings-considered-harmful/index.json +49 -0
- data/public/api/ro/posts/lost-in-the-desert/index.json +49 -0
- data/public/api/ro/posts/mission/index.json +49 -0
- data/public/api/ro/posts/return-your-laptop/index.json +61 -0
- data/public/ro/nerd/fastest-possible-embeddings/assets/giraffe.jpeg +0 -0
- data/public/ro/nerd/fastest-possible-embeddings/assets/let-me-in.jpg +0 -0
- data/public/ro/nerd/fastest-possible-embeddings/assets/src/fastembed.js +70 -0
- data/public/ro/nerd/fastest-possible-embeddings/assets/src/fastembed.rs +68 -0
- data/public/ro/nerd/fastest-possible-embeddings/assets/terminal.jpg +0 -0
- data/public/ro/nerd/fastest-possible-embeddings/body.md +266 -0
- data/public/ro/nerd/fastest-possible-embeddings.yml +7 -0
- data/public/ro/nerd/ima/assets/og.jpeg +0 -0
- data/public/ro/nerd/ima/body.md +22 -0
- data/public/ro/nerd/ima.yml +8 -0
- data/public/ro/nerd/index/assets/giraffe.jpeg +0 -0
- data/public/ro/nerd/index/assets/let-me-in.jpg +0 -0
- data/public/ro/nerd/index/assets/terminal.jpg +0 -0
- data/public/ro/nerd/index/body.md +130 -0
- data/public/ro/nerd/index.yml +7 -0
- data/public/ro/pages/about/assets/og.jpeg +0 -0
- data/public/ro/pages/about/assets/speak-english-pulp-fiction.gif +0 -0
- data/public/ro/pages/about/body.md +40 -0
- data/public/ro/pages/contact/assets/giraffe.jpeg +0 -0
- data/public/ro/pages/contact/body.md +9 -0
- data/public/ro/pages/contact.yml +7 -0
- data/public/ro/pages/cv/assets/ara.jpg +0 -0
- data/public/ro/pages/cv/body.md +122 -0
- data/public/ro/pages/cv.yml +6 -0
- data/public/ro/pages/disco/assets/disco.jpg +0 -0
- data/public/ro/pages/disco/assets/disco.png +0 -0
- data/public/ro/pages/disco/assets/speak-english-pulp-fiction.gif +0 -0
- data/public/ro/pages/disco/assets/src/environment.md +2354 -0
- data/public/ro/pages/disco/assets/src/fortune-500.md +2518 -0
- data/public/ro/pages/disco/assets/src/greed.md +2703 -0
- data/public/ro/pages/disco/assets/src/up-at-night.md +2337 -0
- data/public/ro/pages/disco/body.md +99 -0
- data/public/ro/pages/disco/samples/environment.md +2354 -0
- data/public/ro/pages/disco/samples/fortune-500.md +2518 -0
- data/public/ro/pages/disco/samples/greed.md +2703 -0
- data/public/ro/pages/disco/samples/up-at-night.md +2337 -0
- data/public/ro/pages/disco.yml +9 -0
- data/public/ro/pages/index/body.md +15 -0
- data/public/ro/pages/index.yml +1 -0
- data/public/ro/pages/jess/assets/og.jpg +0 -0
- data/public/ro/pages/jess/assets/speak-english-pulp-fiction.gif +0 -0
- data/public/ro/pages/jess/body.md +3 -0
- data/public/ro/pages/jess.yml +7 -0
- data/public/ro/pages/now/assets/speak-english-pulp-fiction.gif +0 -0
- data/public/ro/pages/now/body.md +24 -0
- data/public/ro/pages/now.yml +1 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image1.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image10.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image11.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image12.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image13.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image14.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image15.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image2.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image3.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image4.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image5.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image6.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image7.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image8.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/image9.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/josh-pointing.jpg +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/levi-rawr.png +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/og.jpg +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/assets/purple-heart.jpg +0 -0
- data/public/ro/posts/almost-died-in-an-ice-cave/body.md +419 -0
- data/public/ro/posts/almost-died-in-an-ice-cave.yml +6 -0
- data/public/ro/posts/facebook-and-global-extremism/assets/background.html +125 -0
- data/public/ro/posts/facebook-and-global-extremism/assets/background.md +95 -0
- data/public/ro/posts/facebook-and-global-extremism/assets/og.jpg +0 -0
- data/public/ro/posts/facebook-and-global-extremism/assets/prompt.txt +122 -0
- data/public/ro/posts/facebook-and-global-extremism/assets/results.md +183 -0
- data/public/ro/posts/facebook-and-global-extremism/assets/survey.txt +190 -0
- data/public/ro/posts/facebook-and-global-extremism/body.md +393 -0
- data/public/ro/posts/facebook-and-global-extremism.yml +7 -0
- data/public/ro/posts/lemmings-considered-harmful/assets/lemming.jpeg +0 -0
- data/public/ro/posts/lemmings-considered-harmful/body.md +43 -0
- data/public/ro/posts/lemmings-considered-harmful.yml +6 -0
- data/public/ro/posts/lost-in-the-desert/assets/og.jpg +0 -0
- data/public/ro/posts/lost-in-the-desert/body.md +7 -0
- data/public/ro/posts/lost-in-the-desert.yml +6 -0
- data/public/ro/posts/mission/assets/og.jpg +0 -0
- data/public/ro/posts/mission/body.md +4 -0
- data/public/ro/posts/mission.yml +6 -0
- data/public/ro/posts/return-your-laptop/assets/og.jpg +0 -0
- data/public/ro/posts/return-your-laptop/assets/return-your-laptop.png +0 -0
- data/public/ro/posts/return-your-laptop/body.md +58 -0
- data/public/ro/posts/return-your-laptop.yml +6 -0
- data/ro.gemspec +369 -49
- data/scripts/speedtest.rb +324 -0
- data/specs/001-simplify-asset-structure/IMPLEMENTATION_SUMMARY.md +212 -0
- data/specs/001-simplify-asset-structure/checklists/requirements.md +36 -0
- data/specs/001-simplify-asset-structure/contracts/collection_api.md +407 -0
- data/specs/001-simplify-asset-structure/contracts/migrator_api.md +461 -0
- data/specs/001-simplify-asset-structure/contracts/node_api.md +294 -0
- data/specs/001-simplify-asset-structure/data-model.md +381 -0
- data/specs/001-simplify-asset-structure/plan.md +90 -0
- data/specs/001-simplify-asset-structure/quickstart.md +575 -0
- data/specs/001-simplify-asset-structure/research.md +333 -0
- data/specs/001-simplify-asset-structure/spec.md +127 -0
- data/specs/001-simplify-asset-structure/tasks.md +349 -0
- data/test/fixtures/new_structure/mixed/test-json.json +5 -0
- data/test/fixtures/new_structure/mixed/test-yaml.yml +3 -0
- data/test/fixtures/new_structure/posts/metadata-only.yml +7 -0
- data/test/fixtures/new_structure/posts/nested-test/assets/subdirectory/image.png +2 -0
- data/test/fixtures/new_structure/posts/nested-test.yml +7 -0
- data/test/fixtures/new_structure/posts/sample-post/assets/body.md +5 -0
- data/test/fixtures/new_structure/posts/sample-post/assets/image.jpg +2 -0
- data/test/fixtures/new_structure/posts/sample-post.yml +7 -0
- data/test/fixtures/old_structure/posts/assets-only/assets/test.txt +1 -0
- data/test/fixtures/old_structure/posts/sample-post/assets/body.md +5 -0
- data/test/fixtures/old_structure/posts/sample-post/assets/image.jpg +2 -0
- data/test/fixtures/old_structure/posts/sample-post/attributes.yml +2 -0
- data/test/integration/ro_integration_test.rb +165 -0
- data/test/test_helper.rb +149 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/attributes.yml +7 -0
- data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post/image.jpg +2 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post.yml +7 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post/body.md +5 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post/image.jpg +2 -0
- data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post.yml +7 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/assets-only/assets/test.txt +1 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/assets-only/assets/test.txt +1 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/assets-only/assets/test.txt +1 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/assets-only/assets/test.txt +1 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/assets/body.md +5 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/assets/image.jpg +2 -0
- data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/attributes.yml +2 -0
- data/test/tmp/new_structure_test_1760746452/mixed/test-json.json +5 -0
- data/test/tmp/new_structure_test_1760746452/mixed/test-yaml.yml +3 -0
- data/test/tmp/new_structure_test_1760746452/posts/metadata-only.yml +7 -0
- data/test/tmp/new_structure_test_1760746452/posts/nested-test/subdirectory/image.png +2 -0
- data/test/tmp/new_structure_test_1760746452/posts/nested-test.yml +7 -0
- data/test/tmp/new_structure_test_1760746452/posts/sample-post/body.md +5 -0
- data/test/tmp/new_structure_test_1760746452/posts/sample-post/image.jpg +2 -0
- data/test/tmp/new_structure_test_1760746452/posts/sample-post.yml +7 -0
- data/test/unit/asset_test.rb +90 -0
- data/test/unit/collection_test.rb +127 -0
- data/test/unit/migrator_test.rb +209 -0
- data/test/unit/node_test.rb +138 -0
- data/tmp/gem-details.oe +0 -0
- metadata +250 -33
- data/public/api/ro/posts/first_post/index.json +0 -52
- data/public/api/ro/posts/second_post/index.json +0 -51
- data/public/api/ro/posts/third_post/index.json +0 -51
- data/public/ro/posts/first_post/assets/foo/bar/baz.jpg +0 -0
- data/public/ro/posts/first_post/assets/foo.jpg +0 -0
- data/public/ro/posts/first_post/assets/src/foo/bar.rb +0 -3
- data/public/ro/posts/first_post/attributes.yml +0 -2
- data/public/ro/posts/first_post/blurb.erb.md +0 -7
- data/public/ro/posts/first_post/body.md +0 -16
- data/public/ro/posts/first_post/testing.txt +0 -3
- data/public/ro/posts/second_post/assets/foo/bar/baz.jpg +0 -0
- data/public/ro/posts/second_post/assets/foo.jpg +0 -0
- data/public/ro/posts/second_post/assets/src/foo/bar.rb +0 -3
- data/public/ro/posts/second_post/attributes.yml +0 -2
- data/public/ro/posts/second_post/blurb.erb.md +0 -5
- data/public/ro/posts/second_post/body.md +0 -16
- data/public/ro/posts/third_post/assets/foo/bar/baz.jpg +0 -0
- data/public/ro/posts/third_post/assets/foo.jpg +0 -0
- data/public/ro/posts/third_post/assets/src/foo/bar.rb +0 -3
- data/public/ro/posts/third_post/attributes.yml +0 -2
- data/public/ro/posts/third_post/blurb.erb.md +0 -5
- data/public/ro/posts/third_post/body.md +0 -16
data/lib/ro/asset.rb
CHANGED
|
@@ -2,14 +2,6 @@ module Ro
|
|
|
2
2
|
class Asset < ::String
|
|
3
3
|
include Klass
|
|
4
4
|
|
|
5
|
-
DEFAULT_IMAGE_PATTERNS = [
|
|
6
|
-
/[.](webp|jpg|jpeg|png|gif|tif|tiff|svg)$/i
|
|
7
|
-
]
|
|
8
|
-
|
|
9
|
-
def Asset.image_patterns
|
|
10
|
-
@image_patterns ||= DEFAULT_IMAGE_PATTERNS.dup
|
|
11
|
-
end
|
|
12
|
-
|
|
13
5
|
attr_reader :path, :node, :relative_path, :name, :url
|
|
14
6
|
|
|
15
7
|
def initialize(arg, *args)
|
|
@@ -17,9 +9,48 @@ module Ro
|
|
|
17
9
|
|
|
18
10
|
@path = Path.for(arg, *args)
|
|
19
11
|
|
|
20
|
-
|
|
12
|
+
# T029: Updated to split on node ID instead of /assets/ segment
|
|
13
|
+
@node = options.fetch(:node) do
|
|
14
|
+
# Try to find node by splitting path
|
|
15
|
+
# In new structure: no /assets/ segment
|
|
16
|
+
# In old structure: /assets/ segment exists
|
|
17
|
+
if @path.to_s.include?('/assets/')
|
|
18
|
+
# Old structure
|
|
19
|
+
Node.for(@path.split('/assets/').first)
|
|
20
|
+
else
|
|
21
|
+
# New structure: find node directory by looking at parent paths
|
|
22
|
+
# Asset path like: /collection/node-id/file.jpg
|
|
23
|
+
# Node path should be: /collection/node-id
|
|
24
|
+
found_node = nil
|
|
25
|
+
node_path = @path.parent
|
|
26
|
+
while node_path && !node_path.basename.to_s.match(/\.(yml|yaml|json|toml)$/)
|
|
27
|
+
# Check if there's a metadata file for this directory
|
|
28
|
+
collection_path = node_path.parent
|
|
29
|
+
node_id = node_path.basename.to_s
|
|
30
|
+
|
|
31
|
+
%w[yml yaml json toml].each do |ext|
|
|
32
|
+
metadata_file = collection_path.join("#{node_id}.#{ext}")
|
|
33
|
+
if metadata_file.exist?
|
|
34
|
+
root = Root.for(collection_path.parent)
|
|
35
|
+
collection = root.collection_for(collection_path)
|
|
36
|
+
found_node = Node.new(collection, metadata_file)
|
|
37
|
+
break
|
|
38
|
+
end
|
|
39
|
+
end
|
|
21
40
|
|
|
22
|
-
|
|
41
|
+
break if found_node
|
|
42
|
+
node_path = node_path.parent
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Fallback: old behavior
|
|
46
|
+
found_node || Node.for(@path.split('/assets/').first)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# T030: Updated relative_path calculation for new structure
|
|
51
|
+
# In new structure, path is already relative to node.asset_dir
|
|
52
|
+
# In old structure, need to account for assets/ prefix
|
|
53
|
+
@relative_path = @path.relative_to(@node.asset_dir)
|
|
23
54
|
|
|
24
55
|
@name = @relative_path
|
|
25
56
|
|
|
@@ -28,18 +59,38 @@ module Ro
|
|
|
28
59
|
super(@path)
|
|
29
60
|
end
|
|
30
61
|
|
|
31
|
-
def
|
|
32
|
-
@path.file? &&
|
|
62
|
+
def is_img?
|
|
63
|
+
@path.file? && Ro.is_image?(@path.basename)
|
|
33
64
|
end
|
|
34
65
|
|
|
35
|
-
|
|
66
|
+
alias is_img is_img?
|
|
67
|
+
|
|
68
|
+
def img
|
|
69
|
+
return unless is_img?
|
|
70
|
+
Ro.image_info(path.to_s)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def is_src?
|
|
36
74
|
key = relative_path.parts
|
|
37
|
-
|
|
38
|
-
|
|
75
|
+
# Check if the first directory in the path is 'src'
|
|
76
|
+
# e.g., src/file.js or src/subdir/file.js
|
|
77
|
+
first_dir = key.size >= 2 ? key[0] : nil
|
|
78
|
+
!!(first_dir == 'src')
|
|
79
|
+
end
|
|
39
80
|
|
|
40
|
-
|
|
81
|
+
alias is_src is_src?
|
|
41
82
|
|
|
83
|
+
def src
|
|
84
|
+
return unless is_src?
|
|
42
85
|
Ro.render_src(path, node)
|
|
43
86
|
end
|
|
87
|
+
|
|
88
|
+
def stat
|
|
89
|
+
@path.stat.size
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def size
|
|
93
|
+
stat.size
|
|
94
|
+
end
|
|
44
95
|
end
|
|
45
96
|
end
|
data/lib/ro/collection.rb
CHANGED
|
@@ -34,6 +34,20 @@ module Ro
|
|
|
34
34
|
Node.new(path)
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# T021: Scan for metadata files in new structure format
|
|
38
|
+
def metadata_files
|
|
39
|
+
extensions = %w[yml yaml json toml]
|
|
40
|
+
files = []
|
|
41
|
+
|
|
42
|
+
extensions.each do |ext|
|
|
43
|
+
@path.glob("*.#{ext}").each do |file|
|
|
44
|
+
files << file if file.file?
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
files.sort
|
|
49
|
+
end
|
|
50
|
+
|
|
37
51
|
def subdirectories(...)
|
|
38
52
|
@path.subdirectories(...)
|
|
39
53
|
end
|
|
@@ -42,13 +56,61 @@ module Ro
|
|
|
42
56
|
@path.subdirectory_for(name)
|
|
43
57
|
end
|
|
44
58
|
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
# T020: Modified to discover nodes from metadata files (new structure)
|
|
60
|
+
def each(offset:nil, limit:nil, &block)
|
|
61
|
+
# Return enumerator if no block given and no offset/limit
|
|
62
|
+
return to_enum(:each, offset: offset, limit: limit) unless block_given?
|
|
63
|
+
|
|
64
|
+
# Use metadata files for new structure instead of subdirectories
|
|
65
|
+
files = metadata_files
|
|
66
|
+
|
|
67
|
+
if offset
|
|
68
|
+
i = -1
|
|
69
|
+
n = 0
|
|
70
|
+
files.each do |metadata_file|
|
|
71
|
+
i += 1
|
|
72
|
+
next if i < offset
|
|
73
|
+
node = Node.new(self, metadata_file)
|
|
74
|
+
block.call(node)
|
|
75
|
+
n += 1
|
|
76
|
+
break if limit && n >= limit
|
|
77
|
+
end
|
|
78
|
+
else
|
|
79
|
+
files.each do |metadata_file|
|
|
80
|
+
node = Node.new(self, metadata_file)
|
|
81
|
+
block.call(node)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
class Page < ::Array
|
|
89
|
+
attr_accessor :number
|
|
47
90
|
|
|
48
|
-
|
|
49
|
-
|
|
91
|
+
def initialize(nodes = [], number: 1)
|
|
92
|
+
replace(nodes)
|
|
93
|
+
@number = number
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def page(number, size: 10)
|
|
98
|
+
offset = [(number - 1), 0].max * size
|
|
99
|
+
limit = [size, 1].max
|
|
100
|
+
|
|
101
|
+
nodes = each(offset:, limit:)
|
|
102
|
+
Page.new(nodes, number:)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def paginate(size: 10, &block)
|
|
106
|
+
number = 0
|
|
107
|
+
accum = []
|
|
50
108
|
|
|
51
|
-
|
|
109
|
+
loop do
|
|
110
|
+
number += 1
|
|
111
|
+
page = self.page(number, size:)
|
|
112
|
+
break if page.empty?
|
|
113
|
+
block ? block.call(page) : accum.push(page)
|
|
52
114
|
end
|
|
53
115
|
|
|
54
116
|
block ? self : accum
|
|
@@ -105,8 +167,10 @@ module Ro
|
|
|
105
167
|
block ? self : accum
|
|
106
168
|
end
|
|
107
169
|
|
|
108
|
-
def to_array(
|
|
109
|
-
|
|
170
|
+
def to_array(offset: nil, limit: nil)
|
|
171
|
+
accum = []
|
|
172
|
+
each(offset: offset, limit: limit) { |node| accum << node }
|
|
173
|
+
accum
|
|
110
174
|
end
|
|
111
175
|
|
|
112
176
|
alias to_a to_array
|
|
@@ -143,11 +207,28 @@ module Ro
|
|
|
143
207
|
]
|
|
144
208
|
end
|
|
145
209
|
|
|
210
|
+
# T022: Modified to find nodes by metadata filename
|
|
146
211
|
def get(name)
|
|
147
|
-
|
|
148
|
-
|
|
212
|
+
# Try to find metadata file for this node ID
|
|
213
|
+
extensions = %w[yml yaml json toml]
|
|
214
|
+
extensions.each do |ext|
|
|
215
|
+
metadata_file = @path.join("#{name}.#{ext}")
|
|
216
|
+
if metadata_file.exist? && metadata_file.file?
|
|
217
|
+
return Node.new(self, metadata_file)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
149
220
|
|
|
150
|
-
|
|
221
|
+
# Also try with slugified versions
|
|
222
|
+
[
|
|
223
|
+
Slug.for(name, :join => '-'),
|
|
224
|
+
Slug.for(name, :join => '_')
|
|
225
|
+
].each do |slug|
|
|
226
|
+
extensions.each do |ext|
|
|
227
|
+
metadata_file = @path.join("#{slug}.#{ext}")
|
|
228
|
+
if metadata_file.exist? && metadata_file.file?
|
|
229
|
+
return Node.new(self, metadata_file)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
151
232
|
end
|
|
152
233
|
|
|
153
234
|
nil
|
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
|
-
|
|
4
|
-
|
|
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
|
data/lib/ro/html_safe.rb
ADDED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
186
|
+
strategies = expand_asset_url_strategies
|
|
187
|
+
error = nil
|
|
170
188
|
|
|
171
|
-
|
|
189
|
+
strategies.each do |strategy|
|
|
172
190
|
return send(strategy, html, node)
|
|
173
191
|
rescue Object => e
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
Ro.log(
|
|
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
|
-
|
|
197
|
+
raise error
|
|
180
198
|
end
|
|
181
199
|
|
|
182
200
|
def accurate_expand_asset_urls(html, node)
|
|
183
|
-
doc =
|
|
201
|
+
doc = Nokogiri::HTML.fragment(html.to_str)
|
|
184
202
|
|
|
185
|
-
doc.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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.
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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
|