Class: Ro::Node

Inherits:
Object show all
Includes:
Klass
Defined in:
lib/ro/node.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Klass

included

Constructor Details

#initialize(collection_or_path, metadata_file = nil) ⇒ Node

T023: Updated to accept (collection, metadata_file) for new structure



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/ro/node.rb', line 8

def initialize(collection_or_path,  = nil)
  if 
    # New structure: collection + metadata_file
    @collection = collection_or_path
     = Path.for()

    # Raise error if metadata file doesn't exist
    unless .exist?
      raise Errno::ENOENT, "No such file or directory - #{@metadata_file}"
    end

    @root = @collection.root

    # Derive node ID from metadata filename (without extension)
    # T025: ID derived from metadata filename
    node_id = .basename.to_s.sub(/\.(yml|yaml|json|toml)$/, '')

    # Path is the node directory (sibling to metadata file)
    @path = @collection.path.join(node_id)
  else
    # Old structure compatibility: just a path
    @path = Path.for(collection_or_path)
    @root = Root.for(@path.parent.parent)
     = nil
  end

  @attributes = :lazyload
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args, &block) ⇒ Object



312
313
314
315
316
317
318
319
320
# File 'lib/ro/node.rb', line 312

def method_missing(method, *args, &block)
  key = method.to_s

  if attributes.has_key?(key)
    attributes[key]
  else
    super
  end
end

Instance Attribute Details

#metadata_fileObject (readonly)

Returns the value of attribute metadata_file.



5
6
7
# File 'lib/ro/node.rb', line 5

def 
  
end

#pathObject (readonly)

Returns the value of attribute path.



5
6
7
# File 'lib/ro/node.rb', line 5

def path
  @path
end

#rootObject (readonly)

Returns the value of attribute root.



5
6
7
# File 'lib/ro/node.rb', line 5

def root
  @root
end

Instance Method Details

#<=>(other) ⇒ Object



354
355
356
# File 'lib/ro/node.rb', line 354

def <=>(other)
  sort_key <=> other.sort_key
end

#[](*args) ⇒ Object



238
239
240
# File 'lib/ro/node.rb', line 238

def [](*args)
  attributes.get(*args)
end

#_ignored_filesObject

T028: Updated ignore patterns for new structure



191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/ro/node.rb', line 191

def _ignored_files
  # Both old and new structure: ignore attributes files and assets/ subdirectory
  ignored_files =
    %w[
      attributes.yml
      attributes.yaml
      attributes.json
      ./assets/**/**
    ].map do |glob|
      @path.glob(glob).select(&:file?)
    end.flatten
end

#_load_asset_attributesObject



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/ro/node.rb', line 103

def _load_asset_attributes
  {}.tap do |hash|
    assets.each do |asset|
      key = asset.name
      url = asset.url
      path = asset.path.relative_to(@root)
      src = asset.src
      img = asset.img
      size = asset.size

      value = { url:, path:, size:, img:, src: }

      hash[key] = value
    end

    @attributes.set(assets: hash)
  end
end

#_load_base_attributesObject

T026: Modified to load from external metadata_file (new structure)



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/ro/node.rb', line 87

def _load_base_attributes
  if  && .exist?
    # New structure: load from explicit metadata file
    attrs = _render()
    update_attributes!(attrs, file: )
  else
    # Old structure: search for attributes.yml in node directory
    glob = "attributes.{yml,yaml,json}"

    @path.glob(glob) do |file|
      attrs = _render(file)
      update_attributes!(attrs, file:)
    end
  end
end

#_load_file_attributesObject



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/ro/node.rb', line 137

def _load_file_attributes
  ignored = _ignored_files

  @path.files.each do |file|
    next if ignored.include?(file)

    rel = file.relative_to(@path)

    key = rel.parts
    basename = key.pop
    base = basename.split('.', 2).first
    key.push(base)

    value = _render(file)

    if value.is_a?(HTML)
      attrs = value.front_matter
      update_attributes!(attrs, file:)
    end

    if @attributes.has?(key)
      raise Error.new("path=#{ @path.inspect } masks #{ key.inspect } in #{ @attributes.inspect }!")
    end

    @attributes.set(key => value)
  end
end

#_load_meta_attributesObject



122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/ro/node.rb', line 122

def _load_meta_attributes
  {}.tap do |hash|
    hash.update(
      identifier:,
      type:,
      id:,
      urls:,
      created_at:,
      updated_at:,
    )

    @attributes.set(_meta: hash)
  end
end

#_render(file) ⇒ Object



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
# File 'lib/ro/node.rb', line 204

def _render(file)
  node = self

  value = Ro.render(file, _render_context)

  if value.is_a?(HTML)
    front_matter = value.front_matter
    html = Ro.expand_asset_urls(value, node)
    value = HTML.new(html, front_matter:)
  end

  if value.is_a?(Hash)
    attributes = value
    value = Ro.expand_asset_values(attributes, node)
  end

  value
end

#_render_contextObject



223
224
225
226
227
228
# File 'lib/ro/node.rb', line 223

def _render_context
  to_hash.tap do |context|
    context[:ro] ||= root
    context[:collection] ||= collection
  end
end

#as_jsonObject



338
339
340
# File 'lib/ro/node.rb', line 338

def as_json(...)
  to_hash.as_json(...)
end

#asset_dirObject

T027: Updated to return assets/ subdirectory in both old and new structure



247
248
249
250
251
# File 'lib/ro/node.rb', line 247

def asset_dir
  # Both old and new structure use assets/ subdirectory
  # This prevents files from being rendered as templates
  path.join('assets')
end

#asset_for(*args) ⇒ Object



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/ro/node.rb', line 265

def asset_for(*args)
  options = Map.options_for!(args)

  path_info = Path.relative(args)

  path = @path.join('assets', path_info)

  glob = path_info.gsub(/[_-]/, '[_-]')

  globs =
    [
      @path.call('assets', "#{glob}"),
      @path.call('assets', "#{glob}*"),
      @path.call('assets', "**/#{glob}*")
    ]

  candidates = globs.map { |glob| Dir.glob(glob, ::File::FNM_CASEFOLD) }.flatten.compact.uniq.sort

  case candidates.size
  when 0
    raise ArgumentError, "no asset matching #{globs.inspect}"
  else
    path = candidates.last
  end

  Asset.for(path, node: self)
end

#asset_for?(*args, &block) ⇒ Boolean

Returns:

  • (Boolean)


293
294
295
296
297
# File 'lib/ro/node.rb', line 293

def asset_for?(*args, &block)
  asset_for(*args, &block)
rescue StandardError
  nil
end

#asset_pathsObject



253
254
255
# File 'lib/ro/node.rb', line 253

def asset_paths
  asset_dir.select { |entry| entry.file? }.sort
end

#asset_urlsObject



261
262
263
# File 'lib/ro/node.rb', line 261

def asset_urls
  assets.map(&:url)
end

#assetsObject



257
258
259
# File 'lib/ro/node.rb', line 257

def assets
  asset_paths.map { |path| Asset.for(path, node: self) }
end

#attributesObject



66
67
68
69
# File 'lib/ro/node.rb', line 66

def attributes
  load_attributes
  @attributes
end

#collectionObject



62
63
64
# File 'lib/ro/node.rb', line 62

def collection
  @collection || @root.collection_for(type)
end

#created_atObject



370
371
372
# File 'lib/ro/node.rb', line 370

def created_at
  files.map{|file| File.stat(file).ctime}.min
end

#default_sort_keyObject



362
363
364
365
366
367
368
# File 'lib/ro/node.rb', line 362

def default_sort_key
  position = (attributes[:position] ? Float(attributes[:position]) : 0.0)
  published_at = (attributes[:published_at] ? Time.parse(attributes[:published_at].to_s) : Time.at(0)).utc.iso8601
  created_at = (attributes[:created_at] ? Time.parse(attributes[:created_at].to_s) : Time.at(0)).utc.iso8601

  [position, published_at, created_at, name]
end

#fetch(*args) ⇒ Object



230
231
232
# File 'lib/ro/node.rb', line 230

def fetch(*args)
  attributes.fetch(*args)
end

#filesObject



346
347
348
# File 'lib/ro/node.rb', line 346

def files
  path.glob('**/**').select { |entry| entry.file? }.sort
end

#get(*args) ⇒ Object



234
235
236
# File 'lib/ro/node.rb', line 234

def get(*args)
  attributes.get(*args)
end

#idObject



46
47
48
# File 'lib/ro/node.rb', line 46

def id
  name
end

#identifierObject



54
55
56
# File 'lib/ro/node.rb', line 54

def identifier
  File.join(type, id)
end

#inspectObject



58
59
60
# File 'lib/ro/node.rb', line 58

def inspect
  identifier
end

#load_attributesObject



71
72
73
# File 'lib/ro/node.rb', line 71

def load_attributes
  load_attributes! if @attributes == :lazyload
end

#load_attributes!Object



75
76
77
78
79
80
81
82
83
84
# File 'lib/ro/node.rb', line 75

def load_attributes!
  @attributes = Map.new

  _load_base_attributes
  _load_file_attributes
  _load_asset_attributes
  _load_meta_attributes

  @attributes
end

#nameObject



37
38
39
40
41
42
43
44
# File 'lib/ro/node.rb', line 37

def name
  if 
    # T025: For new structure, name comes from metadata filename
    .basename.to_s.sub(/\.(yml|yaml|json|toml)$/, '')
  else
    @path.name
  end
end

#path_forObject



303
304
305
# File 'lib/ro/node.rb', line 303

def path_for(...)
  @path.join(...)
end

#relative_pathObject



242
243
244
# File 'lib/ro/node.rb', line 242

def relative_path
  path.relative_to(root)
end

#sort_keyObject



358
359
360
# File 'lib/ro/node.rb', line 358

def sort_key
  default_sort_key
end

#src_for(*args) ⇒ Object



307
308
309
310
# File 'lib/ro/node.rb', line 307

def src_for(*args)
  key = Path.relative(:assets, :src, args).split('/')
  get(key)
end

#to_hashObject



322
323
324
# File 'lib/ro/node.rb', line 322

def to_hash
  attributes.to_hash
end

#to_jsonObject



334
335
336
# File 'lib/ro/node.rb', line 334

def to_json(...)
  JSON.pretty_generate(to_hash, ...)
end

#to_sObject



326
327
328
# File 'lib/ro/node.rb', line 326

def to_s(...)
  to_json(...)
end

#to_strObject



330
331
332
# File 'lib/ro/node.rb', line 330

def to_str(...)
  to_json(...)
end

#to_yamlObject



342
343
344
# File 'lib/ro/node.rb', line 342

def to_yaml(...)
  to_hash.to_yaml(...)
end

#typeObject



50
51
52
# File 'lib/ro/node.rb', line 50

def type
  @path.parent.name
end

#update_attributes!(attrs = {}, **context) ⇒ Object



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/ro/node.rb', line 165

def update_attributes!(attrs = {}, **context)
  attrs = Map.for(attrs)

  blacklist = %w[
    assets
    _meta
  ]

  blacklist.each do |key|
    if attrs.has_key?(key)
      Ro.error!("#{ key } is blacklisted!", **context)
    end
  end

  keys = @attributes.depth_first_keys

  attrs.depth_first_keys.each do |key|
    if keys.include?(key)
      Ro.error!("#{ attrs.inspect } clobbers #{ @attributes.inspect }!", **context)
    end
  end

  @attributes.update(attrs)
end

#updated_atObject



374
375
376
# File 'lib/ro/node.rb', line 374

def updated_at
  files.map{|file| File.stat(file).mtime}.max
end

#url_for(relative_path, options = {}) ⇒ Object



299
300
301
# File 'lib/ro/node.rb', line 299

def url_for(relative_path, options = {})
  Ro.url_for(self.relative_path, relative_path, options)
end

#urlsObject



350
351
352
# File 'lib/ro/node.rb', line 350

def urls
  files.map { |file| url_for(file.relative_to(@path)) }.sort
end