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.
Files changed (252) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +57 -10
  3. data/LICENSE +1 -1
  4. data/MIGRATION.md +320 -0
  5. data/README.md +286 -111
  6. data/Rakefile +2 -2
  7. data/a.yml +60 -0
  8. data/bin/ro +10 -0
  9. data/lib/ro/_lib.rb +18 -6
  10. data/lib/ro/asset.rb +67 -16
  11. data/lib/ro/collection.rb +91 -10
  12. data/lib/ro/config.rb +4 -0
  13. data/lib/ro/error.rb +5 -2
  14. data/lib/ro/html.rb +23 -0
  15. data/lib/ro/html_safe.rb +143 -0
  16. data/lib/ro/methods.rb +95 -38
  17. data/lib/ro/migrator.rb +285 -0
  18. data/lib/ro/node.rb +128 -45
  19. data/lib/ro/path.rb +4 -0
  20. data/lib/ro/root.rb +75 -1
  21. data/lib/ro/script/migrate.rb +204 -0
  22. data/lib/ro/script/server.rb +1 -1
  23. data/lib/ro/template.rb +62 -22
  24. data/lib/ro/text.rb +120 -0
  25. data/lib/ro.rb +5 -0
  26. data/public/api/ro/index-1.json +997 -79
  27. data/public/api/ro/index.json +997 -79
  28. data/public/api/ro/nerd/fastest-possible-embeddings/index.json +90 -0
  29. data/public/api/ro/nerd/ima/index.json +49 -0
  30. data/public/api/ro/nerd/index/index.json +74 -0
  31. data/public/api/ro/nerd/index-1.json +204 -0
  32. data/public/api/ro/nerd/index.json +194 -0
  33. data/public/api/ro/pages/about/index.json +60 -0
  34. data/public/api/ro/pages/contact/index.json +50 -0
  35. data/public/api/ro/pages/cv/index.json +49 -0
  36. data/public/api/ro/pages/disco/index.json +117 -0
  37. data/public/api/ro/pages/index/index.json +30 -0
  38. data/public/api/ro/pages/index-1.json +366 -0
  39. data/public/api/ro/pages/index.json +356 -0
  40. data/public/api/ro/pages/jess/index.json +62 -0
  41. data/public/api/ro/pages/now/index.json +43 -0
  42. data/public/api/ro/posts/almost-died-in-an-ice-cave/index.json +265 -0
  43. data/public/api/ro/posts/facebook-and-global-extremism/index.json +90 -0
  44. data/public/api/ro/posts/index-1.json +461 -79
  45. data/public/api/ro/posts/index.json +461 -79
  46. data/public/api/ro/posts/lemmings-considered-harmful/index.json +49 -0
  47. data/public/api/ro/posts/lost-in-the-desert/index.json +49 -0
  48. data/public/api/ro/posts/mission/index.json +49 -0
  49. data/public/api/ro/posts/return-your-laptop/index.json +61 -0
  50. data/public/ro/nerd/fastest-possible-embeddings/assets/giraffe.jpeg +0 -0
  51. data/public/ro/nerd/fastest-possible-embeddings/assets/let-me-in.jpg +0 -0
  52. data/public/ro/nerd/fastest-possible-embeddings/assets/src/fastembed.js +70 -0
  53. data/public/ro/nerd/fastest-possible-embeddings/assets/src/fastembed.rs +68 -0
  54. data/public/ro/nerd/fastest-possible-embeddings/assets/terminal.jpg +0 -0
  55. data/public/ro/nerd/fastest-possible-embeddings/body.md +266 -0
  56. data/public/ro/nerd/fastest-possible-embeddings.yml +7 -0
  57. data/public/ro/nerd/ima/assets/og.jpeg +0 -0
  58. data/public/ro/nerd/ima/body.md +22 -0
  59. data/public/ro/nerd/ima.yml +8 -0
  60. data/public/ro/nerd/index/assets/giraffe.jpeg +0 -0
  61. data/public/ro/nerd/index/assets/let-me-in.jpg +0 -0
  62. data/public/ro/nerd/index/assets/terminal.jpg +0 -0
  63. data/public/ro/nerd/index/body.md +130 -0
  64. data/public/ro/nerd/index.yml +7 -0
  65. data/public/ro/pages/about/assets/og.jpeg +0 -0
  66. data/public/ro/pages/about/assets/speak-english-pulp-fiction.gif +0 -0
  67. data/public/ro/pages/about/body.md +40 -0
  68. data/public/ro/pages/contact/assets/giraffe.jpeg +0 -0
  69. data/public/ro/pages/contact/body.md +9 -0
  70. data/public/ro/pages/contact.yml +7 -0
  71. data/public/ro/pages/cv/assets/ara.jpg +0 -0
  72. data/public/ro/pages/cv/body.md +122 -0
  73. data/public/ro/pages/cv.yml +6 -0
  74. data/public/ro/pages/disco/assets/disco.jpg +0 -0
  75. data/public/ro/pages/disco/assets/disco.png +0 -0
  76. data/public/ro/pages/disco/assets/speak-english-pulp-fiction.gif +0 -0
  77. data/public/ro/pages/disco/assets/src/environment.md +2354 -0
  78. data/public/ro/pages/disco/assets/src/fortune-500.md +2518 -0
  79. data/public/ro/pages/disco/assets/src/greed.md +2703 -0
  80. data/public/ro/pages/disco/assets/src/up-at-night.md +2337 -0
  81. data/public/ro/pages/disco/body.md +99 -0
  82. data/public/ro/pages/disco/samples/environment.md +2354 -0
  83. data/public/ro/pages/disco/samples/fortune-500.md +2518 -0
  84. data/public/ro/pages/disco/samples/greed.md +2703 -0
  85. data/public/ro/pages/disco/samples/up-at-night.md +2337 -0
  86. data/public/ro/pages/disco.yml +9 -0
  87. data/public/ro/pages/index/body.md +15 -0
  88. data/public/ro/pages/index.yml +1 -0
  89. data/public/ro/pages/jess/assets/og.jpg +0 -0
  90. data/public/ro/pages/jess/assets/speak-english-pulp-fiction.gif +0 -0
  91. data/public/ro/pages/jess/body.md +3 -0
  92. data/public/ro/pages/jess.yml +7 -0
  93. data/public/ro/pages/now/assets/speak-english-pulp-fiction.gif +0 -0
  94. data/public/ro/pages/now/body.md +24 -0
  95. data/public/ro/pages/now.yml +1 -0
  96. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image1.png +0 -0
  97. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image10.png +0 -0
  98. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image11.png +0 -0
  99. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image12.png +0 -0
  100. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image13.png +0 -0
  101. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image14.png +0 -0
  102. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image15.png +0 -0
  103. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image2.png +0 -0
  104. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image3.png +0 -0
  105. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image4.png +0 -0
  106. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image5.png +0 -0
  107. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image6.png +0 -0
  108. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image7.png +0 -0
  109. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image8.png +0 -0
  110. data/public/ro/posts/almost-died-in-an-ice-cave/assets/image9.png +0 -0
  111. data/public/ro/posts/almost-died-in-an-ice-cave/assets/josh-pointing.jpg +0 -0
  112. data/public/ro/posts/almost-died-in-an-ice-cave/assets/levi-rawr.png +0 -0
  113. data/public/ro/posts/almost-died-in-an-ice-cave/assets/og.jpg +0 -0
  114. data/public/ro/posts/almost-died-in-an-ice-cave/assets/purple-heart.jpg +0 -0
  115. data/public/ro/posts/almost-died-in-an-ice-cave/body.md +419 -0
  116. data/public/ro/posts/almost-died-in-an-ice-cave.yml +6 -0
  117. data/public/ro/posts/facebook-and-global-extremism/assets/background.html +125 -0
  118. data/public/ro/posts/facebook-and-global-extremism/assets/background.md +95 -0
  119. data/public/ro/posts/facebook-and-global-extremism/assets/og.jpg +0 -0
  120. data/public/ro/posts/facebook-and-global-extremism/assets/prompt.txt +122 -0
  121. data/public/ro/posts/facebook-and-global-extremism/assets/results.md +183 -0
  122. data/public/ro/posts/facebook-and-global-extremism/assets/survey.txt +190 -0
  123. data/public/ro/posts/facebook-and-global-extremism/body.md +393 -0
  124. data/public/ro/posts/facebook-and-global-extremism.yml +7 -0
  125. data/public/ro/posts/lemmings-considered-harmful/assets/lemming.jpeg +0 -0
  126. data/public/ro/posts/lemmings-considered-harmful/body.md +43 -0
  127. data/public/ro/posts/lemmings-considered-harmful.yml +6 -0
  128. data/public/ro/posts/lost-in-the-desert/assets/og.jpg +0 -0
  129. data/public/ro/posts/lost-in-the-desert/body.md +7 -0
  130. data/public/ro/posts/lost-in-the-desert.yml +6 -0
  131. data/public/ro/posts/mission/assets/og.jpg +0 -0
  132. data/public/ro/posts/mission/body.md +4 -0
  133. data/public/ro/posts/mission.yml +6 -0
  134. data/public/ro/posts/return-your-laptop/assets/og.jpg +0 -0
  135. data/public/ro/posts/return-your-laptop/assets/return-your-laptop.png +0 -0
  136. data/public/ro/posts/return-your-laptop/body.md +58 -0
  137. data/public/ro/posts/return-your-laptop.yml +6 -0
  138. data/ro.gemspec +369 -49
  139. data/scripts/speedtest.rb +324 -0
  140. data/specs/001-simplify-asset-structure/IMPLEMENTATION_SUMMARY.md +212 -0
  141. data/specs/001-simplify-asset-structure/checklists/requirements.md +36 -0
  142. data/specs/001-simplify-asset-structure/contracts/collection_api.md +407 -0
  143. data/specs/001-simplify-asset-structure/contracts/migrator_api.md +461 -0
  144. data/specs/001-simplify-asset-structure/contracts/node_api.md +294 -0
  145. data/specs/001-simplify-asset-structure/data-model.md +381 -0
  146. data/specs/001-simplify-asset-structure/plan.md +90 -0
  147. data/specs/001-simplify-asset-structure/quickstart.md +575 -0
  148. data/specs/001-simplify-asset-structure/research.md +333 -0
  149. data/specs/001-simplify-asset-structure/spec.md +127 -0
  150. data/specs/001-simplify-asset-structure/tasks.md +349 -0
  151. data/test/fixtures/new_structure/mixed/test-json.json +5 -0
  152. data/test/fixtures/new_structure/mixed/test-yaml.yml +3 -0
  153. data/test/fixtures/new_structure/posts/metadata-only.yml +7 -0
  154. data/test/fixtures/new_structure/posts/nested-test/assets/subdirectory/image.png +2 -0
  155. data/test/fixtures/new_structure/posts/nested-test.yml +7 -0
  156. data/test/fixtures/new_structure/posts/sample-post/assets/body.md +5 -0
  157. data/test/fixtures/new_structure/posts/sample-post/assets/image.jpg +2 -0
  158. data/test/fixtures/new_structure/posts/sample-post.yml +7 -0
  159. data/test/fixtures/old_structure/posts/assets-only/assets/test.txt +1 -0
  160. data/test/fixtures/old_structure/posts/sample-post/assets/body.md +5 -0
  161. data/test/fixtures/old_structure/posts/sample-post/assets/image.jpg +2 -0
  162. data/test/fixtures/old_structure/posts/sample-post/attributes.yml +2 -0
  163. data/test/integration/ro_integration_test.rb +165 -0
  164. data/test/test_helper.rb +149 -0
  165. data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/assets/image.jpg +2 -0
  166. data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/attributes.yml +7 -0
  167. data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/body.md +5 -0
  168. data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/assets/image.jpg +2 -0
  169. data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/attributes.yml +7 -0
  170. data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/body.md +5 -0
  171. data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/assets/image.jpg +2 -0
  172. data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/attributes.yml +7 -0
  173. data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/body.md +5 -0
  174. data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/assets/image.jpg +2 -0
  175. data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/attributes.yml +7 -0
  176. data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/body.md +5 -0
  177. data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/assets/image.jpg +2 -0
  178. data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/attributes.yml +7 -0
  179. data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/body.md +5 -0
  180. data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/assets/image.jpg +2 -0
  181. data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/attributes.yml +7 -0
  182. data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/body.md +5 -0
  183. data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post/body.md +5 -0
  184. data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post/image.jpg +2 -0
  185. data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post.yml +7 -0
  186. data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post/body.md +5 -0
  187. data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post/image.jpg +2 -0
  188. data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post.yml +7 -0
  189. data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/assets/body.md +5 -0
  190. data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/assets/image.jpg +2 -0
  191. data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/attributes.yml +2 -0
  192. data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/assets/body.md +5 -0
  193. data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/assets/image.jpg +2 -0
  194. data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/attributes.yml +2 -0
  195. data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/assets/body.md +5 -0
  196. data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/assets/image.jpg +2 -0
  197. data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/attributes.yml +2 -0
  198. data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/assets/body.md +5 -0
  199. data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/assets/image.jpg +2 -0
  200. data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/attributes.yml +2 -0
  201. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/assets-only/assets/test.txt +1 -0
  202. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/assets/body.md +5 -0
  203. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/assets/image.jpg +2 -0
  204. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/attributes.yml +2 -0
  205. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/assets-only/assets/test.txt +1 -0
  206. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/assets/body.md +5 -0
  207. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/assets/image.jpg +2 -0
  208. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/attributes.yml +2 -0
  209. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/assets-only/assets/test.txt +1 -0
  210. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/assets/body.md +5 -0
  211. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/assets/image.jpg +2 -0
  212. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/attributes.yml +2 -0
  213. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/assets-only/assets/test.txt +1 -0
  214. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/assets/body.md +5 -0
  215. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/assets/image.jpg +2 -0
  216. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/attributes.yml +2 -0
  217. data/test/tmp/new_structure_test_1760746452/mixed/test-json.json +5 -0
  218. data/test/tmp/new_structure_test_1760746452/mixed/test-yaml.yml +3 -0
  219. data/test/tmp/new_structure_test_1760746452/posts/metadata-only.yml +7 -0
  220. data/test/tmp/new_structure_test_1760746452/posts/nested-test/subdirectory/image.png +2 -0
  221. data/test/tmp/new_structure_test_1760746452/posts/nested-test.yml +7 -0
  222. data/test/tmp/new_structure_test_1760746452/posts/sample-post/body.md +5 -0
  223. data/test/tmp/new_structure_test_1760746452/posts/sample-post/image.jpg +2 -0
  224. data/test/tmp/new_structure_test_1760746452/posts/sample-post.yml +7 -0
  225. data/test/unit/asset_test.rb +90 -0
  226. data/test/unit/collection_test.rb +127 -0
  227. data/test/unit/migrator_test.rb +209 -0
  228. data/test/unit/node_test.rb +138 -0
  229. data/tmp/gem-details.oe +0 -0
  230. metadata +250 -33
  231. data/public/api/ro/posts/first_post/index.json +0 -52
  232. data/public/api/ro/posts/second_post/index.json +0 -51
  233. data/public/api/ro/posts/third_post/index.json +0 -51
  234. data/public/ro/posts/first_post/assets/foo/bar/baz.jpg +0 -0
  235. data/public/ro/posts/first_post/assets/foo.jpg +0 -0
  236. data/public/ro/posts/first_post/assets/src/foo/bar.rb +0 -3
  237. data/public/ro/posts/first_post/attributes.yml +0 -2
  238. data/public/ro/posts/first_post/blurb.erb.md +0 -7
  239. data/public/ro/posts/first_post/body.md +0 -16
  240. data/public/ro/posts/first_post/testing.txt +0 -3
  241. data/public/ro/posts/second_post/assets/foo/bar/baz.jpg +0 -0
  242. data/public/ro/posts/second_post/assets/foo.jpg +0 -0
  243. data/public/ro/posts/second_post/assets/src/foo/bar.rb +0 -3
  244. data/public/ro/posts/second_post/attributes.yml +0 -2
  245. data/public/ro/posts/second_post/blurb.erb.md +0 -5
  246. data/public/ro/posts/second_post/body.md +0 -16
  247. data/public/ro/posts/third_post/assets/foo/bar/baz.jpg +0 -0
  248. data/public/ro/posts/third_post/assets/foo.jpg +0 -0
  249. data/public/ro/posts/third_post/assets/src/foo/bar.rb +0 -3
  250. data/public/ro/posts/third_post/attributes.yml +0 -2
  251. data/public/ro/posts/third_post/blurb.erb.md +0 -5
  252. data/public/ro/posts/third_post/body.md +0 -16
@@ -0,0 +1,204 @@
1
+ module Ro
2
+ class Script::Migrate
3
+ def self.run!(script:)
4
+ new(script: script).run!
5
+ end
6
+
7
+ attr_accessor :script
8
+
9
+ def initialize(script:)
10
+ @script = script
11
+ end
12
+
13
+ def run!
14
+ parse_options!
15
+
16
+ root_path = get_root_path
17
+
18
+ show_banner(root_path)
19
+
20
+ migrator = Ro::Migrator.new(root_path, @options)
21
+
22
+ validate_and_report(migrator)
23
+
24
+ return if @options[:dry_run]
25
+
26
+ confirm_unless_forced!
27
+
28
+ execute_migration(migrator)
29
+ end
30
+
31
+ private
32
+
33
+ def parse_options!
34
+ @options = {
35
+ dry_run: false,
36
+ backup: true,
37
+ verbose: false,
38
+ force: false
39
+ }
40
+
41
+ # Use ARGV directly because the Ro.script DSL may have consumed flags
42
+ # We need to find 'migrate' in ARGV and parse everything after it
43
+ argv = []
44
+ found_migrate = false
45
+ ARGV.each do |arg|
46
+ if arg == 'migrate'
47
+ found_migrate = true
48
+ next
49
+ end
50
+ argv << arg if found_migrate
51
+ end
52
+
53
+ while argv.any?
54
+ arg = argv.shift
55
+ case arg
56
+ when '-d', '--dry-run'
57
+ @options[:dry_run] = true
58
+ @options[:verbose] = true
59
+ when '-b', '--backup'
60
+ @options[:backup] = true
61
+ when '--no-backup'
62
+ @options[:backup] = false
63
+ when '-v', '--verbose'
64
+ @options[:verbose] = true
65
+ when '-f', '--force'
66
+ @options[:force] = true
67
+ when '-h', '--help'
68
+ show_help
69
+ exit 0
70
+ else
71
+ argv.unshift(arg)
72
+ break
73
+ end
74
+ end
75
+
76
+ @script.argv.replace(argv)
77
+ end
78
+
79
+ def get_root_path
80
+ if @script.argv.any?
81
+ Pathname.new(@script.argv.first).expand_path
82
+ else
83
+ Ro.config.root.expand
84
+ end
85
+ end
86
+
87
+ def show_banner(root_path)
88
+ puts "Ro Asset Structure Migration Tool"
89
+ puts "=" * 50
90
+ puts "Root: #{root_path}"
91
+ puts "Options: #{@options.inspect}" if @options[:verbose]
92
+ puts ""
93
+ end
94
+
95
+ def validate_and_report(migrator)
96
+ puts "Validating structure..."
97
+ validation = migrator.validate
98
+
99
+ puts "Collections found: #{validation[:collections].size}"
100
+ puts "Old structure nodes: #{validation[:old_nodes].size}"
101
+ puts "New structure nodes: #{validation[:new_nodes].size}"
102
+ puts ""
103
+
104
+ if validation[:old_nodes].empty?
105
+ puts "✓ No old structure nodes found - migration not needed"
106
+ exit 0
107
+ end
108
+
109
+ if validation[:has_new_structure]
110
+ if @options[:force]
111
+ puts "⚠ Warning: Both old and new structures detected!"
112
+ puts "Proceeding with partial migration (--force enabled)..."
113
+ puts ""
114
+ else
115
+ puts "⚠ Warning: Both old and new structures detected!"
116
+ puts "This may indicate a partial migration."
117
+ puts "Use --force to proceed anyway, or check your data first."
118
+ exit 1
119
+ end
120
+ end
121
+
122
+ if @options[:dry_run] || @options[:verbose]
123
+ show_preview(migrator)
124
+ end
125
+
126
+ if @options[:dry_run]
127
+ puts "✓ Dry run complete - no changes made"
128
+ exit 0
129
+ end
130
+ end
131
+
132
+ def show_preview(migrator)
133
+ puts "Migration plan:"
134
+ puts "-" * 50
135
+ plan = migrator.preview
136
+ plan.each_with_index do |step, i|
137
+ puts "\n#{i + 1}. #{step[:collection]}/#{step[:node_id]}"
138
+ step[:actions].each do |action|
139
+ puts " - #{action}"
140
+ end
141
+ end
142
+ puts ""
143
+ end
144
+
145
+ def confirm_unless_forced!
146
+ return if @options[:force]
147
+
148
+ print "Proceed with migration? [y/N] "
149
+ response = STDIN.gets.chomp
150
+ unless response.downcase == 'y'
151
+ puts "Migration cancelled"
152
+ exit 0
153
+ end
154
+ end
155
+
156
+ def execute_migration(migrator)
157
+ puts "\nStarting migration..."
158
+ result = migrator.migrate
159
+
160
+ if result[:success]
161
+ puts "\n✓ Migration complete!"
162
+ puts " Nodes migrated: #{result[:nodes_migrated]}"
163
+ puts " Collections migrated: #{result[:collections_migrated]}"
164
+
165
+ if @options[:backup]
166
+ puts "\n Backup created - rollback available if needed"
167
+ end
168
+ else
169
+ puts "\n✗ Migration failed"
170
+ exit 1
171
+ end
172
+ end
173
+
174
+ def show_help
175
+ puts <<~HELP
176
+ Usage: ro migrate [options] [ROOT_PATH]
177
+
178
+ Migrates Ro assets from old structure to new simplified structure
179
+
180
+ Options:
181
+ -d, --dry-run Preview migration without making changes
182
+ -b, --[no-]backup Create backup before migrating (default: true)
183
+ -v, --verbose Show detailed progress
184
+ -f, --force Force migration even if new structure detected
185
+ -h, --help Show this help message
186
+
187
+ Examples:
188
+ # Preview migration
189
+ ro migrate --dry-run
190
+
191
+ # Migrate with backup (default)
192
+ ro migrate
193
+
194
+ # Migrate without backup
195
+ ro migrate --no-backup
196
+
197
+ # Force migration in mixed structure
198
+ ro migrate --force
199
+
200
+ See MIGRATION.md for more details.
201
+ HELP
202
+ end
203
+ end
204
+ end
@@ -9,7 +9,7 @@ module Ro
9
9
  def initialize(script:)
10
10
  @script = script
11
11
 
12
- @port = @script.opts.fetch(:port)
12
+ @port = @script.options.fetch(:port, Ro.defaults.port)
13
13
  end
14
14
 
15
15
  def run!
data/lib/ro/template.rb CHANGED
@@ -5,16 +5,14 @@ module Ro
5
5
  require 'kramdown'
6
6
  require 'kramdown-parser-gfm'
7
7
 
8
+ require_relative 'html.rb'
8
9
  require_relative 'template/rouge_formatter.rb'
9
10
 
10
- class HTML < ::String
11
- end
12
-
13
- def self.render(*args, &block)
11
+ def Template.render(*args, &block)
14
12
  render_file(*args, &block)
15
13
  end
16
14
 
17
- def self.render_file(path, options = {})
15
+ def Template.render_file(path, options = {})
18
16
  path = File.expand_path(path.to_s.strip)
19
17
  options = Map.for(options.is_a?(Hash) ? options : { context: options })
20
18
 
@@ -22,10 +20,15 @@ module Ro
22
20
  engines = File.basename(path).split('.')[1..-1]
23
21
  context = options[:context]
24
22
 
25
- render_string(content, path: path, engines: engines, context: context)
23
+ begin
24
+ render_string(content, path: path, engines: engines, context: context)
25
+ rescue Ro::Error => e
26
+ msg = e.message
27
+ Ro.error! "failed to render #{ path } with `#{ msg }`"
28
+ end
26
29
  end
27
30
 
28
- def self.render_string(content, options = {})
31
+ def Template.render_string(content, options = {})
29
32
  content = String(content).force_encoding('utf-8')
30
33
  options = Map.for(options.is_a?(Hash) ? options : { context: options })
31
34
  engines = Array(options.fetch(:engines) { ['md'] }).flatten.compact
@@ -39,22 +42,22 @@ module Ro
39
42
 
40
43
  content =
41
44
  case engine
42
- when 'txt', 'text', 'html'
43
- content
45
+ when 'html'
46
+ render_html(content)
47
+ when 'txt', 'text'
48
+ render_text(content)
44
49
  when 'erb'
45
50
  render_erb(content, context:)
46
51
  when 'md', 'markdown'
47
52
  render_markdown(content)
48
- when 'yml'
49
- data = YAML.load(content)
50
- pod = Ro.pod(data)
51
- Ro.mapify(data)
53
+ when 'yml', 'yaml'
54
+ render_yaml(content)
52
55
  when 'json'
53
- data = JSON.parse(content)
54
- pod = Ro.pod(data)
55
- Ro.mapify(data)
56
+ render_json(content)
56
57
  when 'rb'
57
- eval(content)
58
+ render_ruby(content)
59
+ when 'txt', 'text'
60
+ render_text(content)
58
61
  else
59
62
  Ro.error!("no engine found for engine=#{ engine.inspect } engines=#{ engines.inspect }")
60
63
  end
@@ -63,7 +66,36 @@ module Ro
63
66
  content
64
67
  end
65
68
 
66
- def self.render_erb(content, options = {})
69
+ def Template.render_html(html)
70
+ HTML.new(html)
71
+ end
72
+
73
+ def Template.render_json(json)
74
+ data = JSON.parse(json)
75
+ Ro.mapify(data)
76
+ end
77
+
78
+ def Template.render_yaml(yaml)
79
+ data = YAML.load(yaml)
80
+ Ro.mapify(data)
81
+ end
82
+
83
+ def Template.render_ruby(code)
84
+ string = IO.popen('ruby', 'w+'){|ruby| ruby.puts(code); ruby.close_write; ruby.read}
85
+
86
+ if $? == 0
87
+ string
88
+ else
89
+ Ro.error!("ruby:\n\n#{ code }")
90
+ end
91
+ end
92
+
93
+ def Template.render_text(text)
94
+ html = Text.render(text)
95
+ HTML.new(html)
96
+ end
97
+
98
+ def Template.render_erb(content, options = {})
67
99
  content = String(content).force_encoding('utf-8')
68
100
  options = Map.for(options.is_a?(Hash) ? options : { context: options })
69
101
  context = options[:context]
@@ -82,27 +114,35 @@ module Ro
82
114
  HTML.new(html)
83
115
  end
84
116
 
85
- def self.render_markdown(content, options = {})
117
+ def Template.render_markdown(content, options = {})
86
118
  content = String(content).force_encoding('utf-8')
87
119
  options = Map.for(options.is_a?(Hash) ? options : { context: options })
88
120
 
89
121
  theme = options.fetch(:theme) { Ro.config.md_theme }
90
122
 
123
+ parsed = FrontMatterParser::Parser.new(:md).call(content)
124
+ front_matter = parsed.front_matter
125
+ content = parsed.content
126
+
91
127
  opts = {
92
128
  input: 'GFM',
129
+ hard_wrap: false,
93
130
  syntax_highlighter_opts: { formatter: RougeFormatter, theme: theme }
94
131
  }
95
132
 
96
- HTML.new(
133
+ div =
97
134
  <<~_____
98
135
  <div class="ro markdown">
99
136
  #{ ::Kramdown::Document.new(content, opts).to_html }
100
137
  </div>
101
138
  _____
102
- )
139
+
140
+ html = Rinku.auto_link(div, :all, 'target="_blank"')
141
+
142
+ HTML.new(html, front_matter:)
103
143
  end
104
144
 
105
- def self.render_src(path, options = {})
145
+ def Template.render_src(path, options = {})
106
146
  path = File.expand_path(path.to_s.strip)
107
147
  options = Map.for(options.is_a?(Hash) ? options : { context: options })
108
148
 
data/lib/ro/text.rb ADDED
@@ -0,0 +1,120 @@
1
+ module Ro
2
+ require 'cgi'
3
+
4
+ module Text
5
+ def render(text)
6
+ text = text.to_s.strip
7
+ return '' if text.empty?
8
+
9
+ text = escape_html(text)
10
+ text = auto_link_urls(text)
11
+ text = auto_link_email_addresses(text)
12
+ text = paragraphs_for(text)
13
+
14
+ return text
15
+ end
16
+
17
+ def paragraphs_for(text)
18
+ paragraphs = text.split(%r`\n\n+`).map{|chunk| lines_for(chunk)}
19
+ paragraphs.map{|p| "<p>\n#{ p }\n</p>"}.join("\n\n").strip
20
+ end
21
+
22
+ def lines_for(text)
23
+ text.strip.gsub(/\n/, "<br />\n")
24
+ end
25
+
26
+ def escape_html(text)
27
+ CGI.escapeHTML(text)
28
+ end
29
+
30
+ AUTO_LINK_RE = %r{
31
+ (?: ((?:ed2k|ftp|http|https|irc|mailto|news|gopher|nntp|telnet|webcal|xmpp|callto|feed|svn|urn|aim|rsync|tag|ssh|sftp|rtsp|afs|file):)// | www\.\w )
32
+ [^\s<\u00A0"]+
33
+ }ix
34
+
35
+ # regexps for determining context, used high-volume
36
+ AUTO_LINK_CRE = [/<[^>]+$/, /^[^>]*>/, /<a\b.*?>/i, /<\/a>/i]
37
+
38
+ AUTO_EMAIL_LOCAL_RE = /[\w.!#\$%&'*\/=?^`{|}~+-]/
39
+ AUTO_EMAIL_RE = /(?<!#{AUTO_EMAIL_LOCAL_RE})[\w.!#\$%+-]\.?#{AUTO_EMAIL_LOCAL_RE}*@[\w-]+(?:\.[\w-]+)+/
40
+
41
+ BRACKETS = { ']' => '[', ')' => '(', '}' => '{' }
42
+
43
+ WORD_PATTERN = '\p{Word}'
44
+
45
+ # Turns all urls into clickable links. If a block is given, each url
46
+ # is yielded and the result is used as the link text.
47
+ def auto_link_urls(text)
48
+ text.gsub(AUTO_LINK_RE) do
49
+ scheme, href = $1, $&
50
+ punctuation = []
51
+ trailing_gt = ""
52
+
53
+ if auto_linked?($`, $')
54
+ # do not change string; URL is already linked
55
+ href
56
+ else
57
+ # don't include trailing punctuation character as part of the URL
58
+ while href.sub!(/[^#{WORD_PATTERN}\/\-=;]$/, '')
59
+ punctuation.push $&
60
+ if opening = BRACKETS[punctuation.last] and href.scan(opening).size > href.scan(punctuation.last).size
61
+ href << punctuation.pop
62
+ break
63
+ end
64
+ end
65
+
66
+ # don't include trailing &gt; entities as part of the URL
67
+ trailing_gt = $& if href.sub!(/&gt;$/, '')
68
+ href = 'http://' + href unless scheme
69
+ link = escape_html(href)
70
+ "<a href='#{ link }'>#{ link }</a>" + punctuation.reverse.join('') + trailing_gt
71
+ end
72
+ end
73
+ end
74
+
75
+ # Turns all email addresses into clickable links. If a block is given,
76
+ # each email is yielded and the result is used as the link text.
77
+ def auto_link_email_addresses(text)
78
+ text.gsub(AUTO_EMAIL_RE) do
79
+ text = $&
80
+
81
+ if auto_linked?($`, $')
82
+ text
83
+ else
84
+ email = escape_html(text)
85
+ "<a href='mailto:#{ email }'>#{ email }</a>"
86
+ end
87
+ end
88
+ end
89
+
90
+ # Detects already linked context or position in the middle of a tag
91
+ def auto_linked?(left, right)
92
+ (left =~ AUTO_LINK_CRE[0] and right =~ AUTO_LINK_CRE[1]) or
93
+ (left.rindex(AUTO_LINK_CRE[2]) and $' !~ AUTO_LINK_CRE[3])
94
+ end
95
+
96
+ extend self
97
+ end
98
+ end
99
+
100
+
101
+ if $0 == __FILE__
102
+ text = <<~____
103
+ this is a paragraph that is very long this is a paragraph that is very long this is a paragraph that is very long this is a paragraph that is very long
104
+ this is a paragraph that is very long this is a paragraph that is very long this is a paragraph that is very long this is a paragraph that is very long
105
+
106
+ this is a paragraph that is very long this is a paragraph that is very long this is a paragraph that is very long this is a paragraph that is very long
107
+
108
+ - a
109
+ - b
110
+ - c
111
+
112
+ & < > me
113
+
114
+ http://drawohara.io>
115
+
116
+ ara.t.howard@gmail.com
117
+ ____
118
+
119
+ puts Ro::Text.render(text)
120
+ end
data/lib/ro.rb CHANGED
@@ -31,6 +31,7 @@ module Ro
31
31
  :root => ENV['RO_ROOT'],
32
32
  :build => ENV['RO_BUILD'],
33
33
  :url => ENV['RO_URL'],
34
+ :img_url => ENV['RO_IMG_URL'],
34
35
  :page_size => ENV['RO_PAGE_SIZE'],
35
36
  :log => ENV['RO_LOG'],
36
37
  :debug => ENV['RO_DEBUG'],
@@ -44,6 +45,7 @@ module Ro
44
45
  :root => './public/ro',
45
46
  :build => './public/api/ro',
46
47
  :url => "/ro",
48
+ :img_url => "/ro",
47
49
  :page_size => 42,
48
50
  :log => nil,
49
51
  :debug => nil,
@@ -61,10 +63,12 @@ module Ro
61
63
  # v
62
64
  def initialize!
63
65
  Ro.load %w[
66
+ html.rb
64
67
  error.rb
65
68
  klass.rb
66
69
  slug.rb
67
70
  path.rb
71
+ text.rb
68
72
  template.rb
69
73
  methods.rb
70
74
  config.rb
@@ -73,6 +77,7 @@ module Ro
73
77
  collection/list.rb
74
78
  node.rb
75
79
  asset.rb
80
+ migrator.rb
76
81
  ]
77
82
 
78
83
  if defined?(ActiveModel)