Module: Scriptorium::Helpers

Includes:
Exceptions
Included in:
API, BannerSVG, Post, Reddit, Repo, Repo, StandardFiles, Theme, Theme, View, Widget
Defined in:
lib/scriptorium/helpers.rb,
lib/skeleton.rb

Overview

Helpers

Instance Method Summary collapse

Methods included from Exceptions

#make_exception

Instance Method Details

#cf_time(t1, t2) ⇒ Object



305
306
307
308
309
310
311
# File 'lib/scriptorium/helpers.rb', line 305

def cf_time(t1, t2)
  t1 = t1.split(/- :/, 6)
  t2 = t2.split(/- :/, 6)
  t1 = Time.new(*t1)
  t2 = Time.new(*t2)
  t1 <=> t2
end

#change_config(file_path, target_key, new_value) ⇒ Object



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/scriptorium/helpers.rb', line 186

def change_config(file_path, target_key, new_value)
  pattern = /
    ^(?<leading>\s*#{Regexp.escape(target_key)}\s+)  # key and spacing
    (?<old_value>[^\#]*?)                            # value (non-greedy up to comment)
    (?<trailing>\s*)                                 # trailing space
    (?<comment>\#.*)?$                               # optional comment
  /x

  lines = read_file(file_path, lines: true)
  updated_lines = lines.map do |line|
    if match = pattern.match(line)
      leading  = match[:leading]
      trailing = match[:trailing]
      comment  = match[:comment] || ''
      "#{leading}#{new_value}#{trailing}#{comment}\n"
    else
      line
    end
  end

  write_file(file_path, updated_lines.join)
end

#clean_slugify(title) ⇒ Object



217
218
219
220
221
222
223
224
225
# File 'lib/scriptorium/helpers.rb', line 217

def clean_slugify(title)
  return "title-is-missing" if title.nil?
  
  slug = title.downcase.strip
             .gsub(/[^a-z0-9\s_-]/, '')  # remove punctuation
             .gsub(/[\s_-]+/, '-')       # replace spaces and underscores with hyphen
             .gsub(/^-+|-+$/, '')        # trim leading/trailing hyphens
  slug
end

#copy_gem_asset_to_user(asset_name, target_dir = "assets") ⇒ Object



406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
# File 'lib/scriptorium/helpers.rb', line 406

def copy_gem_asset_to_user(asset_name, target_dir = "assets")
  begin
    gem_spec = Gem.loaded_specs['scriptorium']
    if gem_spec
      gem_asset_path = "#{gem_spec.full_gem_path}/assets/#{asset_name}"
      if File.exist?(gem_asset_path)
        # Create target directory if it doesn't exist
        FileUtils.mkdir_p(target_dir) unless Dir.exist?(target_dir)
        
        # Copy the asset
        target_path = "#{target_dir}/#{File.basename(asset_name)}"
        FileUtils.cp(gem_asset_path, target_path)
        return target_path
      end
    end
  rescue => e
    # If gem lookup fails, return nil
  end
  nil
end

#copy_to_clipboard(text) ⇒ Object

Clipboard helper methods



428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
# File 'lib/scriptorium/helpers.rb', line 428

def copy_to_clipboard(text)
  begin
    require 'clipboard'
    Clipboard.copy(text)
    true
  rescue LoadError => e
    # Fallback to system commands if clipboard gem not available
    case RbConfig::CONFIG['host_os']
    when /darwin/     # macOS
      system("echo '#{text}' | pbcopy")
    when /linux/      # Linux
      system("echo '#{text}' | xclip -selection clipboard")
    when /mswin|mingw|cygwin/  # Windows
      system("echo '#{text}' | clip")
    else
      puts "Clipboard not supported on this OS"
      false
    end
  rescue => e
    puts "Failed to copy to clipboard: #{e.message}"
    false
  end
end

#d4(num) ⇒ Object



33
34
35
# File 'lib/scriptorium/helpers.rb', line 33

def d4(num)
  "%04d" % num
end

#escape_html(str) ⇒ Object



285
286
287
288
289
290
291
# File 'lib/scriptorium/helpers.rb', line 285

def escape_html(str)
  str.gsub(/&/, '&amp;')
     .gsub(/</, '&lt;')
     .gsub(/>/, '&gt;')
     .gsub(/"/, '&quot;')
     .gsub(/'/, '&#39;')
end

#generate_missing_asset_svg(filename, width: 200, height: 150) ⇒ Object



349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
# File 'lib/scriptorium/helpers.rb', line 349

def generate_missing_asset_svg(filename, width: 200, height: 150)
  # Truncate filename if too long for display
  display_name = filename.length > 20 ? filename[0..16] + "..." : filename
  
  # Generate SVG with broken image icon and filename
  svg = <<~SVG
    <svg width="#{width}" height="#{height}" xmlns="http://www.w3.org/2000/svg">
      <!-- Background -->
      <rect fill="#f8f9fa" stroke="#ddd" stroke-width="1" width="#{width}" height="#{height}" rx="4"/>
      
      <!-- Broken image icon -->
      <g transform="translate(#{width/2}, #{height/2 - 20})">
        <!-- Image frame -->
        <rect x="-15" y="-10" width="30" height="20" fill="none" stroke="#999" stroke-width="1"/>
        <!-- Broken corner -->
        <path d="M 15 -10 L 25 -20 M 15 -10 L 25 0" stroke="#999" stroke-width="1" fill="none"/>
        <!-- Image icon -->
        <rect x="-12" y="-7" width="24" height="14" fill="#e9ecef"/>
        <circle cx="-5" cy="-2" r="2" fill="#999"/>
        <polygon points="-8,8 -2,2 2,6 8,0" fill="#999"/>
      </g>
      
      <!-- Filename -->
      <text x="#{width/2}" y="#{height/2 + 15}" text-anchor="middle" fill="#666" font-family="Arial, sans-serif" font-size="11">
        #{escape_html(display_name)}
      </text>
      
      <!-- "Asset not found" message -->
      <text x="#{width/2}" y="#{height/2 + 30}" text-anchor="middle" fill="#999" font-family="Arial, sans-serif" font-size="9">
        Asset not found
      </text>
    </svg>
  SVG
  
  svg.strip
end

#get_asset_path(name) ⇒ Object



313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# File 'lib/scriptorium/helpers.rb', line 313

def get_asset_path(name)
  if Scriptorium::Repo.testing
    # Development/testing: Check dev_assets first, then local assets
    if File.exist?("dev_assets/#{name}")
      return "dev_assets/#{name}"
    elsif File.exist?("assets/#{name}")
      return "assets/#{name}"
    else
      raise AssetNotFound(name)
    end
  else  # Production
    # Production: Check user assets first, then gem assets
    
    # Check user assets first (highest priority)
    if File.exist?("assets/#{name}")
      return "assets/#{name}"
    end
    
    # Then check gem assets (fallback)
    begin
      gem_spec = Gem.loaded_specs['scriptorium']
      if gem_spec
        gem_asset_path = "#{gem_spec.full_gem_path}/assets/#{name}"
        if File.exist?(gem_asset_path)
          return gem_asset_path
        end
      end
    rescue => e
      # If gem lookup fails, continue without gem assets
    end
    
    # Asset not found
    raise AssetNotFound(name)
  end
end

#get_from_clipboardObject



452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
# File 'lib/scriptorium/helpers.rb', line 452

def get_from_clipboard
  begin
    require 'clipboard'
    Clipboard.paste
  rescue LoadError => e
    # Fallback to system commands if clipboard gem not available
    case RbConfig::CONFIG['host_os']
    when /darwin/     # macOS
      `pbpaste`
    when /linux/      # Linux
      `xclip -selection clipboard -o`
    when /mswin|mingw|cygwin/  # Windows
      `powershell -command "Get-Clipboard"`
    else
      puts "Clipboard not supported on this OS"
      nil
    end
  rescue => e
    puts "Failed to read from clipboard: #{e.message}"
    nil
  end
end

#getvars(file) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
# File 'lib/scriptorium/helpers.rb', line 21

def getvars(file)
  lines = read_file(file, lines: true)
  lines.map! {|line| line.sub(/# .*$/, "").strip }
  lines.reject! {|line| line.empty? }
  vhash = Hash.new("")
  lines.each do |line|
    var, val = line.split(" ", 2)
    vhash[var.to_sym] = val
  end
  vhash
end

#list_gem_assetsObject



386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
# File 'lib/scriptorium/helpers.rb', line 386

def list_gem_assets
  assets = []
  begin
    gem_spec = Gem.loaded_specs['scriptorium']
    if gem_spec
      gem_assets_dir = "#{gem_spec.full_gem_path}/assets"
      if Dir.exist?(gem_assets_dir)
        Dir.glob("#{gem_assets_dir}/**/*").each do |file|
          next unless File.file?(file)
          relative_path = file.sub("#{gem_assets_dir}/", "")
          assets << relative_path
        end
      end
    end
  rescue => e
    # If gem lookup fails, return empty array
  end
  assets.sort
end

#make_dir(dir, create_parents = false) ⇒ Object

Raises:

  • (CannotCreateDirectoryPathNil)


75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/scriptorium/helpers.rb', line 75

def make_dir(dir, create_parents = false)
  # Input validation
  raise CannotCreateDirectoryPathNil if dir.nil?
  
  raise CannotCreateDirectoryPathEmpty if dir.to_s.strip.empty?
  
  # Create parent directories if requested
  if create_parents
    FileUtils.mkdir_p(dir)
  else
    # Create single directory with error handling
    begin
      Dir.mkdir(dir)
    rescue Errno::ENOSPC => e
      raise CannotCreateDirectoryDiskFull(dir, e.message)
    rescue Errno::EACCES => e
      raise CannotCreateDirectoryPermissionDenied(dir, e.message)
    rescue Errno::ENOENT => e
      raise CannotCreateDirectoryParentNotFound(dir, e.message)
    rescue Errno::EEXIST => e
      # Directory already exists - this is usually not an error
      # But we could make this configurable if needed
    rescue => e
      raise CannotCreateDirectoryError(dir, e.message)
    end
  end
end

#make_tree(base, text) ⇒ Object



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/scriptorium/helpers.rb', line 241

def make_tree(base, text)
  lines = text.split("\n").map(&:chomp)
  lines.each {|line| line.gsub!(/ *#.*$/, "") }
  entries = []

  # Determine the root name
  first_line = lines.shift
  root = first_line.strip.sub(/\/$/, "") # remove trailing slash
  root_path = File.join(base, root)
  make_dir(root_path) unless File.exist?(root_path)

  # Prepare stack starting from root
  stack = [root_path]

  # Parse the remaining lines
  lines.each do |line|
    if (i = line.index(/ [a-zA-Z0-9_.]/))
      name = line[(i + 1)..-1]
      level = i / 4
    else
      name = line.strip
      level = 0
    end
    entries << [level, name]
  end

  entries.each do |level, name|
    stack = stack[0..level]
    full_path = File.join(stack.last, name)

    if name.end_with?("/")
      make_dir(full_path) unless File.exist?(full_path)
      stack << full_path
    else
      write_file(full_path, "Empty file generated at #{Time.now}")
    end
  end
end

#need(type, path, exception_class = RuntimeError) ⇒ Object



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/scriptorium/helpers.rb', line 120

def need(type, path, exception_class = RuntimeError)
  # Input validation
  raise CannotRequirePathNil(type) if path.nil?
  
  raise CannotRequirePathEmpty(type) if path.to_s.strip.empty?
  
  # Check if file/directory exists
  exists = case type
           when :file
             File.exist?(path)
           when :dir
             Dir.exist?(path)
           else
             raise InvalidType(type)
           end
  
  unless exists
    raise RequiredFileNotFound(type, path) if exception_class == RuntimeError
    
    # Exception class - try to call it as a method first, then as constructor
    raise exception_class.call(path) if exception_class.respond_to?(:call)
    raise exception_class.new(path)
  end
  
  path
end

#read_commented_file(file_path) ⇒ Object



293
294
295
296
297
298
299
300
301
302
303
# File 'lib/scriptorium/helpers.rb', line 293

def read_commented_file(file_path)
  return [] unless File.exist?(file_path)
  lines = read_file(file_path, lines: true)  # Read file and remove newline characters
  lines.reject! do |line|    # Remove empty lines and comments
    line.strip.empty? || line.strip.start_with?("#")
  end
  lines.map! do |line|       # Strip trailing comments + preceding spaces
    line.sub(/# .*$/, "").strip  
  end
  lines  # Return cleaned lines
end

#read_file(file, options = {}) ⇒ Object

Raises:

  • (CannotReadFilePathNil)


147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/scriptorium/helpers.rb', line 147

def read_file(file, options = {})
  # Input validation
  raise CannotReadFilePathNil if file.nil?
  
  raise CannotReadFilePathEmpty if file.to_s.strip.empty?
  
  # Handle missing file with fallback
  if options[:missing_fallback]
    return options[:missing_fallback] unless File.exist?(file)
  end
  
  # Read the file with error handling
  begin
    if options[:lines]
      # Read as lines
      if options[:chomp]
        File.readlines(file, chomp: true)
      else
        File.readlines(file)
      end
    else
      # Read as content
      File.read(file)
    end
  rescue Errno::ENOENT => e
    if options[:missing_fallback]
      return options[:missing_fallback]
    else
      raise CannotReadFileNotFound(file, e.message)
    end
  rescue Errno::EACCES => e
    raise CannotReadFilePermissionDenied(file, e.message)
  rescue => e
    raise CannotReadFileError(file, e.message)
  end
end

#see(label, var) ⇒ Object



237
238
239
# File 'lib/scriptorium/helpers.rb', line 237

def see(label, var)
  puts "#{label} = \n<<<\n#{var}\n>>>"
end

#see_file(file) ⇒ Object

Really from TestHelpers



231
232
233
234
235
# File 'lib/scriptorium/helpers.rb', line 231

def see_file(file)   # Really from TestHelpers
  puts "----- File: #{file}"
  system!("cat #{file}", "displaying file contents")
  puts "-----"
end

#slugify(id, title) ⇒ Object



209
210
211
212
213
214
215
# File 'lib/scriptorium/helpers.rb', line 209

def slugify(id, title)
  slug = title.downcase.strip
             .gsub(/[^a-z0-9\s_-]/, '')  # remove punctuation
             .gsub(/[\s_-]+/, '-')       # replace spaces and underscores with hyphen
             .gsub(/^-+|-+$/, '')        # trim leading/trailing hyphens
  "#{d4(id)}-#{slug}"
end

#substitute(obj, text) ⇒ Object



280
281
282
283
# File 'lib/scriptorium/helpers.rb', line 280

def substitute(obj, text)
  vars = obj.is_a?(Hash) ? obj : obj.vars
  text % vars
end

#system!(command, description = nil) ⇒ Object

Raises:

  • (CannotExecuteCommandNil)


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

def system!(command, description = nil)
  # Input validation
  raise CannotExecuteCommandNil if command.nil?
  
  raise CannotExecuteCommandEmpty if command.to_s.strip.empty?
  
  # Execute command with error handling
  success = system(command)
  
  unless success
    desc = description ? " (#{description})" : ""
    raise CommandFailedWithDesc(desc, command)
  end
  
  success
end

#view_dir(name) ⇒ Object



37
38
39
# File 'lib/scriptorium/helpers.rb', line 37

def view_dir(name)
  @root/:views/name
end

#write_file(file, content) ⇒ Object

Raises:

  • (CannotWriteFilePathNil)


41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/scriptorium/helpers.rb', line 41

def write_file(file, content)
  # Input validation
  raise CannotWriteFilePathNil if file.nil?
  
  raise CannotWriteFilePathEmpty if file.to_s.strip.empty?
  
  # Ensure parent directory exists
  FileUtils.mkdir_p(File.dirname(file))
  
  # Write the file with error handling
  begin
    File.open(file, "w") do |f|
      f.puts content
    end
  rescue Errno::ENOSPC => e
    raise CannotWriteFileDiskFull(file, e.message)
  rescue Errno::EACCES => e
    raise CannotWriteFilePermissionDenied(file, e.message)
  rescue Errno::ENOENT => e
    raise CannotWriteFileDirectoryNotFound(file, e.message)
  rescue => e
    raise CannotWriteFileError(file, e.message)
  end
end

#write_file!(file, *lines) ⇒ Object



66
67
68
69
70
71
72
73
# File 'lib/scriptorium/helpers.rb', line 66

def write_file!(file, *lines)
  # Convert nil values to empty strings for proper joining
  processed_lines = lines.map { |line| line.nil? ? "" : line.to_s }
  content = processed_lines.join("\n")
  # Always add a newline at the end to ensure there's an empty line
  content += "\n"
  write_file(file, content)
end

#ymdhmsObject



227
228
229
# File 'lib/scriptorium/helpers.rb', line 227

def ymdhms
  Time.now.strftime("%Y-%m-%d-%H-%M-%S")
end