ro 4.4.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 (121) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +42 -16
  3. data/MIGRATION.md +320 -0
  4. data/README.md +30 -18
  5. data/a.yml +60 -0
  6. data/bin/ro +10 -0
  7. data/lib/ro/_lib.rb +1 -1
  8. data/lib/ro/asset.rb +46 -5
  9. data/lib/ro/collection.rb +51 -13
  10. data/lib/ro/migrator.rb +285 -0
  11. data/lib/ro/node.rb +53 -13
  12. data/lib/ro/root.rb +75 -1
  13. data/lib/ro/script/migrate.rb +204 -0
  14. data/lib/ro/script/server.rb +1 -1
  15. data/lib/ro.rb +1 -0
  16. data/ro.gemspec +207 -16
  17. data/specs/001-simplify-asset-structure/IMPLEMENTATION_SUMMARY.md +212 -0
  18. data/specs/001-simplify-asset-structure/checklists/requirements.md +36 -0
  19. data/specs/001-simplify-asset-structure/contracts/collection_api.md +407 -0
  20. data/specs/001-simplify-asset-structure/contracts/migrator_api.md +461 -0
  21. data/specs/001-simplify-asset-structure/contracts/node_api.md +294 -0
  22. data/specs/001-simplify-asset-structure/data-model.md +381 -0
  23. data/specs/001-simplify-asset-structure/plan.md +90 -0
  24. data/specs/001-simplify-asset-structure/quickstart.md +575 -0
  25. data/specs/001-simplify-asset-structure/research.md +333 -0
  26. data/specs/001-simplify-asset-structure/spec.md +127 -0
  27. data/specs/001-simplify-asset-structure/tasks.md +349 -0
  28. data/test/fixtures/new_structure/mixed/test-json.json +5 -0
  29. data/test/fixtures/new_structure/mixed/test-yaml.yml +3 -0
  30. data/test/fixtures/new_structure/posts/metadata-only.yml +7 -0
  31. data/test/fixtures/new_structure/posts/nested-test/assets/subdirectory/image.png +2 -0
  32. data/test/fixtures/new_structure/posts/nested-test.yml +7 -0
  33. data/test/fixtures/new_structure/posts/sample-post/assets/body.md +5 -0
  34. data/test/fixtures/new_structure/posts/sample-post/assets/image.jpg +2 -0
  35. data/test/fixtures/new_structure/posts/sample-post.yml +7 -0
  36. data/test/fixtures/old_structure/posts/assets-only/assets/test.txt +1 -0
  37. data/test/fixtures/old_structure/posts/sample-post/assets/body.md +5 -0
  38. data/test/fixtures/old_structure/posts/sample-post/assets/image.jpg +2 -0
  39. data/test/fixtures/old_structure/posts/sample-post/attributes.yml +2 -0
  40. data/test/integration/ro_integration_test.rb +165 -0
  41. data/test/test_helper.rb +149 -0
  42. data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/assets/image.jpg +2 -0
  43. data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/attributes.yml +7 -0
  44. data/test/tmp/migration_test_1760746513.backup.20251018001513/migration_test_1760746513/posts/sample-post/body.md +5 -0
  45. data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/assets/image.jpg +2 -0
  46. data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/attributes.yml +7 -0
  47. data/test/tmp/migration_test_1760746513.backup.20251018001513/posts/sample-post/body.md +5 -0
  48. data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/assets/image.jpg +2 -0
  49. data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/attributes.yml +7 -0
  50. data/test/tmp/migration_test_1760746556.backup.20251018001556/migration_test_1760746556/posts/sample-post/body.md +5 -0
  51. data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/assets/image.jpg +2 -0
  52. data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/attributes.yml +7 -0
  53. data/test/tmp/migration_test_1760746556.backup.20251018001556/posts/sample-post/body.md +5 -0
  54. data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/assets/image.jpg +2 -0
  55. data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/attributes.yml +7 -0
  56. data/test/tmp/migration_test_1760755248.backup.20251018024048/migration_test_1760755248/posts/sample-post/body.md +5 -0
  57. data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/assets/image.jpg +2 -0
  58. data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/attributes.yml +7 -0
  59. data/test/tmp/migration_test_1760755248.backup.20251018024048/posts/sample-post/body.md +5 -0
  60. data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post/body.md +5 -0
  61. data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post/image.jpg +2 -0
  62. data/test/tmp/migration_test_1760758803.backup.20251018034003/migration_test_1760758803/posts/sample-post.yml +7 -0
  63. data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post/body.md +5 -0
  64. data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post/image.jpg +2 -0
  65. data/test/tmp/migration_test_1760758803.backup.20251018034003/posts/sample-post.yml +7 -0
  66. data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/assets/body.md +5 -0
  67. data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/assets/image.jpg +2 -0
  68. data/test/tmp/migration_test_1760758869.backup.20251018034109/migration_test_1760758869/posts/sample-post/attributes.yml +2 -0
  69. data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/assets/body.md +5 -0
  70. data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/assets/image.jpg +2 -0
  71. data/test/tmp/migration_test_1760758869.backup.20251018034109/posts/sample-post/attributes.yml +2 -0
  72. data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/assets/body.md +5 -0
  73. data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/assets/image.jpg +2 -0
  74. data/test/tmp/migration_test_1760758920.backup.20251018034200/migration_test_1760758920/posts/sample-post/attributes.yml +2 -0
  75. data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/assets/body.md +5 -0
  76. data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/assets/image.jpg +2 -0
  77. data/test/tmp/migration_test_1760758920.backup.20251018034200/posts/sample-post/attributes.yml +2 -0
  78. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/assets-only/assets/test.txt +1 -0
  79. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/assets/body.md +5 -0
  80. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/assets/image.jpg +2 -0
  81. data/test/tmp/migration_test_1760824728.backup.20251018215848/migration_test_1760824728/posts/sample-post/attributes.yml +2 -0
  82. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/assets-only/assets/test.txt +1 -0
  83. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/assets/body.md +5 -0
  84. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/assets/image.jpg +2 -0
  85. data/test/tmp/migration_test_1760824728.backup.20251018215848/posts/sample-post/attributes.yml +2 -0
  86. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/assets-only/assets/test.txt +1 -0
  87. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/assets/body.md +5 -0
  88. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/assets/image.jpg +2 -0
  89. data/test/tmp/migration_test_1760844153.backup.20251019032233/migration_test_1760844153/posts/sample-post/attributes.yml +2 -0
  90. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/assets-only/assets/test.txt +1 -0
  91. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/assets/body.md +5 -0
  92. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/assets/image.jpg +2 -0
  93. data/test/tmp/migration_test_1760844153.backup.20251019032233/posts/sample-post/attributes.yml +2 -0
  94. data/test/tmp/new_structure_test_1760746452/mixed/test-json.json +5 -0
  95. data/test/tmp/new_structure_test_1760746452/mixed/test-yaml.yml +3 -0
  96. data/test/tmp/new_structure_test_1760746452/posts/metadata-only.yml +7 -0
  97. data/test/tmp/new_structure_test_1760746452/posts/nested-test/subdirectory/image.png +2 -0
  98. data/test/tmp/new_structure_test_1760746452/posts/nested-test.yml +7 -0
  99. data/test/tmp/new_structure_test_1760746452/posts/sample-post/body.md +5 -0
  100. data/test/tmp/new_structure_test_1760746452/posts/sample-post/image.jpg +2 -0
  101. data/test/tmp/new_structure_test_1760746452/posts/sample-post.yml +7 -0
  102. data/test/unit/asset_test.rb +90 -0
  103. data/test/unit/collection_test.rb +127 -0
  104. data/test/unit/migrator_test.rb +209 -0
  105. data/test/unit/node_test.rb +138 -0
  106. metadata +111 -18
  107. /data/public/ro/nerd/{fastest-possible-embeddings/attributes.yml → fastest-possible-embeddings.yml} +0 -0
  108. /data/public/ro/nerd/{ima/attributes.yml → ima.yml} +0 -0
  109. /data/public/ro/nerd/{index/attributes.yml → index.yml} +0 -0
  110. /data/public/ro/pages/{contact/attributes.yml → contact.yml} +0 -0
  111. /data/public/ro/pages/{cv/attributes.yml → cv.yml} +0 -0
  112. /data/public/ro/pages/{disco/attributes.yml → disco.yml} +0 -0
  113. /data/public/ro/pages/{index/attributes.yml → index.yml} +0 -0
  114. /data/public/ro/pages/{jess/attributes.yml → jess.yml} +0 -0
  115. /data/public/ro/pages/{now/attributes.yml → now.yml} +0 -0
  116. /data/public/ro/posts/{almost-died-in-an-ice-cave/attributes.yml → almost-died-in-an-ice-cave.yml} +0 -0
  117. /data/public/ro/posts/{facebook-and-global-extremism/attributes.yml → facebook-and-global-extremism.yml} +0 -0
  118. /data/public/ro/posts/{lemmings-considered-harmful/attributes.yml → lemmings-considered-harmful.yml} +0 -0
  119. /data/public/ro/posts/{lost-in-the-desert/attributes.yml → lost-in-the-desert.yml} +0 -0
  120. /data/public/ro/posts/{mission/attributes.yml → mission.yml} +0 -0
  121. /data/public/ro/posts/{return-your-laptop/attributes.yml → return-your-laptop.yml} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9b56a7cf51b2b6e2140b3be71875fe284e77a59d0e7fa108fd45d39369aba51b
4
- data.tar.gz: 02da6783adba57026d22c2e95841ad3b3acc4979c538bd86896b7f293037178b
3
+ metadata.gz: f467beebd987ebb866a7f57a04810ca0c9c4f55396ca07607788c9c614919531
4
+ data.tar.gz: 73ccfea5d2a5d0d8eac25c1a508c3d16de2d52d44dc6b6d250f2e6e1b5616a2e
5
5
  SHA512:
6
- metadata.gz: b704d4f58a18df10308884c353dc05293d294d46ddfb46b8241e5047691500dcf5471f5dac5522a8126e59885c61b49fd03deba036e54db6f2191145706f0450
7
- data.tar.gz: 3bcf32a9c1096c33c898b582f4d038625771c44203ce49792017d07b097285327ce6e90f0c3136bfdda1e185c0d826a50eefbf377dfd2e6e9fc126b8bfed80bf
6
+ metadata.gz: 4f625e44529a3e41f0d9e1f7ddd324ab439be33e364487bf48a8c9d01611694143f80d5747b0432fea673a2a598692bdbb760306a7a6370f5abb006c3ef23a1b
7
+ data.tar.gz: be8bf1fdb7ea9e0ba6b66095c767c6ce4d68c3680cab4fbe8c01cf1e5f85aad011542c94c8478bbf35dab2ec431befd0e7c343a06ff53b54386049e7f42fb013
data/Gemfile.lock CHANGED
@@ -1,14 +1,17 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ro (4.2.1)
5
- ak47 (~> 0.2)
6
- image_size (~> 3.0)
4
+ ro (4.4.1)
5
+ ak47 (~> 0.2.5)
6
+ front_matter_parser (~> 1.0)
7
+ image_size (~> 3.4)
7
8
  kramdown (~> 2.4, >= 2.4.0)
8
9
  kramdown-parser-gfm (~> 1.1, >= 1.1.0)
9
10
  map (~> 6.6, >= 6.6.0)
11
+ nokogiri (~> 1)
12
+ rinku (~> 2.0)
10
13
  rouge (~> 4.1, >= 4.1.1)
11
- webrick (~> 1.8.1)
14
+ webrick (~> 1.9, >= 1.9.1)
12
15
 
13
16
  GEM
14
17
  remote: https://rubygems.org/
@@ -17,17 +20,18 @@ GEM
17
20
  guard (~> 0.10.0)
18
21
  shell_tools (~> 0.1.0)
19
22
  smart_colored
20
- ffi (1.17.0)
21
- ffi (1.17.0-aarch64-linux-gnu)
22
- ffi (1.17.0-aarch64-linux-musl)
23
- ffi (1.17.0-arm-linux-gnu)
24
- ffi (1.17.0-arm-linux-musl)
25
- ffi (1.17.0-arm64-darwin)
26
- ffi (1.17.0-x86-linux-gnu)
27
- ffi (1.17.0-x86-linux-musl)
28
- ffi (1.17.0-x86_64-darwin)
29
- ffi (1.17.0-x86_64-linux-gnu)
30
- ffi (1.17.0-x86_64-linux-musl)
23
+ ffi (1.17.2)
24
+ ffi (1.17.2-aarch64-linux-gnu)
25
+ ffi (1.17.2-aarch64-linux-musl)
26
+ ffi (1.17.2-arm-linux-gnu)
27
+ ffi (1.17.2-arm-linux-musl)
28
+ ffi (1.17.2-arm64-darwin)
29
+ ffi (1.17.2-x86-linux-gnu)
30
+ ffi (1.17.2-x86-linux-musl)
31
+ ffi (1.17.2-x86_64-darwin)
32
+ ffi (1.17.2-x86_64-linux-gnu)
33
+ ffi (1.17.2-x86_64-linux-musl)
34
+ front_matter_parser (1.0.1)
31
35
  guard (0.10.0)
32
36
  ffi (>= 0.5.0)
33
37
  thor (~> 0.14.6)
@@ -37,12 +41,34 @@ GEM
37
41
  kramdown-parser-gfm (1.1.0)
38
42
  kramdown (~> 2.0)
39
43
  map (6.6.0)
44
+ mini_portile2 (2.8.8)
45
+ nokogiri (1.18.8)
46
+ mini_portile2 (~> 2.8.2)
47
+ racc (~> 1.4)
48
+ nokogiri (1.18.8-aarch64-linux-gnu)
49
+ racc (~> 1.4)
50
+ nokogiri (1.18.8-aarch64-linux-musl)
51
+ racc (~> 1.4)
52
+ nokogiri (1.18.8-arm-linux-gnu)
53
+ racc (~> 1.4)
54
+ nokogiri (1.18.8-arm-linux-musl)
55
+ racc (~> 1.4)
56
+ nokogiri (1.18.8-arm64-darwin)
57
+ racc (~> 1.4)
58
+ nokogiri (1.18.8-x86_64-darwin)
59
+ racc (~> 1.4)
60
+ nokogiri (1.18.8-x86_64-linux-gnu)
61
+ racc (~> 1.4)
62
+ nokogiri (1.18.8-x86_64-linux-musl)
63
+ racc (~> 1.4)
64
+ racc (1.8.1)
40
65
  rexml (3.3.9)
66
+ rinku (2.0.6)
41
67
  rouge (4.4.0)
42
68
  shell_tools (0.1.2)
43
69
  smart_colored (1.1.1)
44
70
  thor (0.14.6)
45
- webrick (1.8.2)
71
+ webrick (1.9.1)
46
72
 
47
73
  PLATFORMS
48
74
  aarch64-linux-gnu
data/MIGRATION.md ADDED
@@ -0,0 +1,320 @@
1
+ # Ro v5.0 Asset Structure Migration Guide
2
+
3
+ ## Overview
4
+
5
+ Ro v5.0 introduces a simplified asset directory structure that reduces nesting depth and makes assets easier to understand and manage.
6
+
7
+ ### Automatic Warning System
8
+
9
+ Ro v5.0 automatically detects when you're using old structure data and warns you on stderr:
10
+
11
+ ```
12
+ ⚠️ WARNING: Old Ro asset structure detected!
13
+
14
+ This Ro root contains assets in the OLD structure format:
15
+ • identifier/attributes.yml
16
+ • identifier/assets/
17
+
18
+ Ro v5.0 uses a simplified NEW structure:
19
+ • identifier.yml
20
+ • identifier/
21
+
22
+ Collections will NOT automatically discover old-structure nodes.
23
+
24
+ To migrate your data, run:
25
+ ro migrate /path/to/your/data
26
+ ```
27
+
28
+ **This warning does NOT stop your program** - it's informational to help you know you need to migrate. The warning appears once per root directory when `Ro::Root.new` is called.
29
+
30
+ ### Old Structure (v4.x)
31
+ ```
32
+ posts/
33
+ sample-post/
34
+ attributes.yml # Metadata file INSIDE node directory
35
+ assets/ # Assets subdirectory
36
+ image.jpg
37
+ document.pdf
38
+ ```
39
+
40
+ ### New Structure (v5.0)
41
+ ```
42
+ posts/
43
+ sample-post.yml # Metadata file at COLLECTION level (moved out)
44
+ sample-post/ # Node directory (same location)
45
+ assets/ # Assets subdirectory (SAME location as before!)
46
+ image.jpg
47
+ document.pdf
48
+ ```
49
+
50
+ ## Benefits
51
+
52
+ - **Simpler**: One less level of nesting
53
+ - **Clearer**: Metadata file and assets directory are siblings, not parent/child
54
+ - **Faster**: Easier to locate assets by identifier
55
+ - **More Intuitive**: Structure is self-documenting
56
+
57
+ ## Migration Tool
58
+
59
+ Ro v5.0 includes a migration tool to automate the conversion from old to new structure.
60
+
61
+ ### Basic Usage
62
+
63
+ ```bash
64
+ # Preview migration (dry run)
65
+ ./bin/ro migrate --dry-run /path/to/your/ro/root
66
+
67
+ # Run migration with backup (recommended)
68
+ ./bin/ro migrate /path/to/your/ro/root
69
+
70
+ # Run migration without backup (not recommended)
71
+ ./bin/ro migrate --no-backup /path/to/your/ro/root
72
+ ```
73
+
74
+ ### Options
75
+
76
+ - `--dry-run`, `-d`: Preview changes without making them
77
+ - `--backup`, `--no-backup`, `-b`: Create backup before migrating (default: true)
78
+ - `--verbose`, `-v`: Show detailed progress
79
+ - `--force`, `-f`: Force migration even if new structure detected
80
+ - `--help`, `-h`: Show help message
81
+
82
+ ### Migration Process
83
+
84
+ The migration tool:
85
+
86
+ 1. **Validates** the structure to detect old/new/mixed formats
87
+ 2. **Creates a backup** (unless `--no-backup` is specified)
88
+ 3. **For each old-structure node directory**:
89
+ - **If node has attributes file**: Moves `identifier/attributes.yml` → `identifier.yml` (at collection level)
90
+ - **If node has NO attributes**: Creates empty `identifier.yml` with `{}` content
91
+ - Assets remain in `identifier/assets/` (no change needed!)
92
+
93
+ **Important**: The migrator processes ALL node directories, even those without an attributes file. This ensures every node is discoverable in the new structure.
94
+
95
+ ### Safety Features
96
+
97
+ - **Automatic backups**: Creates timestamped backup before migration
98
+ - **Dry run mode**: Preview changes before applying
99
+ - **Validation**: Detects mixed structures and warns
100
+ - **Rollback support**: Can restore from backup if needed
101
+
102
+ ## Manual Migration
103
+
104
+ If you prefer to migrate manually:
105
+
106
+ 1. **For each node WITH attributes**:
107
+ ```bash
108
+ cd posts
109
+
110
+ # Move metadata file out to collection level
111
+ mv sample-post/attributes.yml sample-post.yml
112
+
113
+ # Assets stay in sample-post/assets/
114
+ ```
115
+
116
+ 2. **For each node WITHOUT attributes** (assets-only):
117
+ ```bash
118
+ cd posts
119
+
120
+ # Create empty metadata file at collection level
121
+ echo '{}' > orphan-assets.yml
122
+
123
+ # Assets stay in orphan-assets/assets/
124
+ ```
125
+
126
+ 3. **Update your code** to use Ro v5.0
127
+
128
+ ## Rollback
129
+
130
+ If you need to rollback after migration:
131
+
132
+ ```ruby
133
+ # Using the Migrator class
134
+ migrator = Ro::Migrator.new('/path/to/ro/root')
135
+ migrator.rollback
136
+ ```
137
+
138
+ This will restore from the most recent backup.
139
+
140
+ ## Migration Checklist
141
+
142
+ - [ ] Review migration plan with `--dry-run`
143
+ - [ ] Backup your data (migration creates backup automatically)
144
+ - [ ] Run migration: `./bin/ro migrate /path/to/ro/root`
145
+ - [ ] Verify migrated data loads correctly
146
+ - [ ] Test your application with new structure
147
+ - [ ] Update code to Ro v5.0 if needed
148
+ - [ ] Remove old backups once confident
149
+
150
+ ## Breaking Changes
151
+
152
+ ### API Changes
153
+
154
+ **Node initialization**:
155
+ ```ruby
156
+ # Old (v4.x) - still works for backward compatibility
157
+ node = Ro::Node.new(node_directory_path)
158
+
159
+ # New (v5.0) - preferred for new structure
160
+ node = Ro::Node.new(collection, metadata_file)
161
+ ```
162
+
163
+ **Asset paths**:
164
+ ```ruby
165
+ # Both old and new structure
166
+ node.asset_dir # => identifier/assets/
167
+
168
+ # The assets/ subdirectory stays the same!
169
+ # Only the metadata file location changes
170
+ ```
171
+
172
+ ### What Stays the Same
173
+
174
+ - **Metadata format**: YAML/JSON/TOML structure unchanged
175
+ - **Asset access**: `node.assets`, `node.asset_paths` work the same
176
+ - **Collection access**: `root['collection']['node']` unchanged
177
+ - **Attribute access**: `node[:title]`, `node.attributes` unchanged
178
+
179
+ ## Testing
180
+
181
+ Run tests to verify migration:
182
+
183
+ ```bash
184
+ # Unit tests
185
+ ruby test/unit/collection_test.rb
186
+ ruby test/unit/node_test.rb
187
+ ruby test/unit/asset_test.rb
188
+ ruby test/unit/migrator_test.rb
189
+
190
+ # Integration tests
191
+ ruby test/integration/ro_integration_test.rb
192
+ ```
193
+
194
+ ## Troubleshooting
195
+
196
+ ### "Both old and new structures detected"
197
+
198
+ This indicates a partial migration. Options:
199
+ 1. Use `--force` to continue migration
200
+ 2. Manually inspect and resolve conflicts
201
+ 3. Restore from backup and retry
202
+
203
+ ### "Node directory not found"
204
+
205
+ Ensure you're running the migration from the correct root directory.
206
+
207
+ ### Assets not loading after migration
208
+
209
+ Check that:
210
+ 1. Metadata files are at collection level (e.g., `posts/sample-post.yml`)
211
+ 2. Node directories exist at collection level (e.g., `posts/sample-post/`)
212
+ 3. Assets are in the `assets/` subdirectory (e.g., `posts/sample-post/assets/image.jpg`)
213
+
214
+ ## Examples
215
+
216
+ ### Example 1: Single Collection
217
+
218
+ Before:
219
+ ```
220
+ ro_data/
221
+ posts/
222
+ welcome/
223
+ attributes.yml
224
+ assets/
225
+ banner.jpg
226
+ ```
227
+
228
+ After:
229
+ ```
230
+ ro_data/
231
+ posts/
232
+ welcome.yml ← Metadata moved out
233
+ welcome/
234
+ assets/
235
+ banner.jpg ← Assets stay in same location
236
+ ```
237
+
238
+ ### Example 2: Multiple Collections
239
+
240
+ Before:
241
+ ```
242
+ ro_data/
243
+ posts/
244
+ post-1/
245
+ attributes.yml
246
+ assets/
247
+ image.jpg
248
+ pages/
249
+ about/
250
+ attributes.yml
251
+ assets/
252
+ photo.png
253
+ ```
254
+
255
+ After:
256
+ ```
257
+ ro_data/
258
+ posts/
259
+ post-1.yml
260
+ post-1/
261
+ assets/
262
+ image.jpg
263
+ pages/
264
+ about.yml
265
+ about/
266
+ assets/
267
+ photo.png
268
+ ```
269
+
270
+ ### Example 3: Metadata-Only Node
271
+
272
+ Before:
273
+ ```
274
+ ro_data/
275
+ posts/
276
+ text-only/
277
+ attributes.yml
278
+ ```
279
+
280
+ After:
281
+ ```
282
+ ro_data/
283
+ posts/
284
+ text-only.yml
285
+ ```
286
+
287
+ ### Example 4: Assets-Only Node (No Attributes)
288
+
289
+ Before:
290
+ ```
291
+ ro_data/
292
+ posts/
293
+ orphan-assets/ ← Directory with no attributes.yml
294
+ assets/
295
+ image.jpg
296
+ ```
297
+
298
+ After:
299
+ ```
300
+ ro_data/
301
+ posts/
302
+ orphan-assets.yml ← Empty metadata file created: {}
303
+ orphan-assets/
304
+ assets/
305
+ image.jpg
306
+ ```
307
+
308
+ ## Support
309
+
310
+ For issues or questions:
311
+ - GitHub Issues: https://github.com/ahoward/ro/issues
312
+ - Documentation: See README.md and code comments
313
+
314
+ ## Version Compatibility
315
+
316
+ - **Ro v4.x**: Old structure only
317
+ - **Ro v5.0**: Both structures (with backward compatibility)
318
+ - **Ro v6.0+**: New structure only (planned)
319
+
320
+ **Recommendation**: Migrate to new structure before Ro v6.0 release.
data/README.md CHANGED
@@ -9,7 +9,7 @@ it is one part cms, one part database, one part api, and one part magic.
9
9
 
10
10
  if you used a headless cms, it's like that, before they existed, except way better, and free.
11
11
 
12
- with `ro`, you can seperate your content from presentation, like a sane and decent human being.
12
+ with `ro`, you can separate your content from presentation, like a sane and decent human being.
13
13
 
14
14
  > silent code whispers
15
15
  > github's gentle, peaceful hush
@@ -37,15 +37,16 @@ for a real site.
37
37
  ```sh
38
38
 
39
39
  drawohara@drawohara.dev:ro[main] #=> tree public/ro/posts/almost-died-in-an-ice-cave/
40
- public/ro/posts/almost-died-in-an-ice-cave/
41
- ├── assets
42
- │ ├── image1.png
43
- ├── image2.png
44
- │ ├── image3.png
45
- │ ├── og.jpg
46
- └── purple-heart.jpg
47
- ├── attributes.yml
48
- └── body.md
40
+ public/ro/posts/
41
+ ├── almost-died-in-an-ice-cave.yml # ← metadata file at collection level
42
+ └── almost-died-in-an-ice-cave/ # ← node directory
43
+ ├── assets/ # ← assets subdirectory
44
+ │ ├── image1.png
45
+ │ ├── image2.png
46
+ ├── image3.png
47
+ ├── og.jpg
48
+ └── purple-heart.jpg
49
+ └── body.md # ← other content files
49
50
 
50
51
  ```
51
52
 
@@ -53,15 +54,22 @@ in this example you can see a few things, regarding the layout of a `ro` directo
53
54
 
54
55
  - `ro` content often, but is not required, to live in `public`. more on this below.
55
56
 
56
- - the essential layout is
57
+ - the essential layout is (v5.0+):
57
58
 
58
59
  ```ruby
59
60
 
60
61
  @root = "ro"
61
62
  @collection = "posts"
62
- @id = "almost-died-in-an-ice-cave"
63
+ @id = "almost-died-in-an-ice-cave"
63
64
 
64
- "#{ @root }/#{ @collection }/#{ @id }"
65
+ # metadata file
66
+ "#{ @root }/#{ @collection }/#{ @id }.yml"
67
+
68
+ # node directory
69
+ "#{ @root }/#{ @collection }/#{ @id }/"
70
+
71
+ # assets
72
+ "#{ @root }/#{ @collection }/#{ @id }/assets/"
65
73
 
66
74
  ```
67
75
 
@@ -85,7 +93,7 @@ if you learn best by example, you can examine the `ro` directory of my own websi
85
93
 
86
94
  more about this can be rtfm'd here -> https://github.com/ahoward/ro/blob/main/lib/ro.rb#L29-L55
87
95
 
88
- because not having a repl sucks, so `ro` has one. you should too.
96
+ because not having a repl sucks, `ro` has one. you should too.
89
97
 
90
98
  ```sh
91
99
 
@@ -158,7 +166,7 @@ you will end up with html that looks like so:
158
166
 
159
167
  <a href='/ro/posts/foo-bar/assets/report.pdf'>linky</a>
160
168
 
161
- <img src='/ro/posts/foo-bar/assets/pretty.png'>alty</a>
169
+ <img src='/ro/posts/foo-bar/assets/pretty.png' alt='alty'>
162
170
 
163
171
 
164
172
  ```
@@ -195,13 +203,13 @@ attributes will be expanded too, at any depth. eg. -> https://github.com/ahowar
195
203
  this means code like this:
196
204
 
197
205
  ```yaml
198
- # file: public/ro/posts/foo-bar/attributes.yml
206
+ # file: public/ro/posts/foo-bar.yml
199
207
 
200
208
  og:
201
209
  image: ./assets/og.jpg
202
210
  ```
203
211
 
204
- will 'just work'. of course, this assumes that 'public/ro/foo-bar/assets/og.jpg' exists!
212
+ will 'just work'. of course, this assumes that 'public/ro/posts/foo-bar/assets/og.jpg' exists!
205
213
 
206
214
  the code is rather robust here, and is not a simple string bashing approach.
207
215
 
@@ -266,7 +274,7 @@ lil detail about that link^, it is from *10 years ago*, when dojo4 was still run
266
274
 
267
275
  ---
268
276
 
269
- one super comman pattern, is to add a build step to a github workflow, and
277
+ one super common pattern, is to add a build step to a github workflow, and
270
278
  then publish the js api via gh-pages. see an example here:
271
279
 
272
280
  https://github.com/ahoward/ro/blob/main/.github/workflows/gh-pages.yml
@@ -339,3 +347,7 @@ https://github.com/LGUG2Z/komorebi-license
339
347
  AI
340
348
  --
341
349
  suck it 🤖s!
350
+
351
+ REF
352
+ ---
353
+ https://github.com/ahoward/ro
data/a.yml ADDED
@@ -0,0 +1,60 @@
1
+ # Simple workflow for deploying static content to GitHub Pages
2
+ name: deploy ro content, api, and site to gh-pages
3
+
4
+ on:
5
+ # Runs on pushes targeting the default branch
6
+ push:
7
+ branches: ["main"]
8
+
9
+ # Allows you to run this workflow manually from the Actions tab
10
+ workflow_dispatch:
11
+
12
+ # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
13
+ permissions:
14
+ contents: read
15
+ pages: write
16
+ id-token: write
17
+
18
+ # allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
19
+ # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
20
+ concurrency:
21
+ group: "pages"
22
+ cancel-in-progress: false
23
+
24
+ jobs:
25
+ # single deploy job since we're just deploying
26
+ deploy:
27
+ environment:
28
+ name: github-pages
29
+ url: ${{ steps.deployment.outputs.page_url }}
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - name: checkout
33
+ # NOTE: this enables git-lfs and caching
34
+ # - https://github.com/actions/checkout/issues/834
35
+ # - https://github.com/nschloe/action-cached-lfs-checkout
36
+ uses: nschloe/action-cached-lfs-checkout@v1
37
+ # you may use this instead, if you don't want git-lfs caching
38
+ #uses: actions/checkout@v4
39
+ #with:
40
+ # lfs: true
41
+ - name: setup gh-pages
42
+ uses: actions/configure-pages@v3
43
+ id: gh-pages
44
+ - name: ro build
45
+ uses: ruby/setup-ruby@v1
46
+ with:
47
+ bundler-cache: true
48
+ - run: |
49
+ export RO_URL=${{ steps.gh-pages.outputs.base_url }}/ro
50
+ export RO_PAGE_SIZE=2
51
+ bundle exec ./bin/ro build ./public/ro ./public/api/ro
52
+ bundle exec ro site public
53
+ tree ./public
54
+ - name: upload artifact
55
+ uses: actions/upload-pages-artifact@v4
56
+ with:
57
+ path: "./public"
58
+ - name: deploy gh-pages
59
+ id: deployment
60
+ uses: actions/deploy-pages@v2
data/bin/ro CHANGED
@@ -130,6 +130,10 @@ Ro.script do
130
130
  site!
131
131
  end
132
132
 
133
+ run(:migrate) do
134
+ migrate!
135
+ end
136
+
133
137
  def setup!(*which)
134
138
  setup_env!
135
139
 
@@ -259,6 +263,12 @@ Ro.script do
259
263
  index_html.binwrite(html)
260
264
  puts index_html
261
265
  end
266
+
267
+ def migrate!
268
+ require "#{$libdir}/ro/script/migrate.rb"
269
+
270
+ Ro::Script::Migrate.run!(script: self)
271
+ end
262
272
  end
263
273
 
264
274
  BEGIN {
data/lib/ro/_lib.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module Ro
2
- VERSION = '4.4.0' unless defined?(VERSION)
2
+ VERSION = '5.0.0' unless defined?(VERSION)
3
3
 
4
4
  class << self
5
5
  def version
data/lib/ro/asset.rb CHANGED
@@ -9,9 +9,48 @@ module Ro
9
9
 
10
10
  @path = Path.for(arg, *args)
11
11
 
12
- @node = options.fetch(:node) { Node.for(@path.split('/assets/').first) }
13
-
14
- @relative_path = @path.relative_to(@node.path)
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
40
+
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)
15
54
 
16
55
  @name = @relative_path
17
56
 
@@ -33,8 +72,10 @@ module Ro
33
72
 
34
73
  def is_src?
35
74
  key = relative_path.parts
36
- subdir = key.size > 2 ? key[1] : nil
37
- !!(subdir == 'src')
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')
38
79
  end
39
80
 
40
81
  alias is_src is_src?