Class: Scriptorium::API

Inherits:
Object
  • Object
show all
Includes:
Contract, Exceptions, Helpers
Defined in:
lib/scriptorium/api.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Contract

#assume, #check_invariants, enabled?, #invariant, #verify

Methods included from Helpers

#cf_time, #change_config, #clean_slugify, #copy_gem_asset_to_user, #copy_to_clipboard, #d4, #escape_html, #generate_missing_asset_svg, #get_asset_path, #get_from_clipboard, #getvars, #list_gem_assets, #make_dir, #make_tree, #need, #read_commented_file, #read_file, #see, #see_file, #slugify, #substitute, #system!, #view_dir, #write_file, #write_file!, #ymdhms

Methods included from Exceptions

#make_exception

Constructor Details

#initialize(testmode: false) ⇒ API

Returns a new instance of API.



14
15
16
17
18
19
20
21
22
23
# File 'lib/scriptorium/api.rb', line 14

def initialize(testmode: false)
  assume { [true, false].include?(testmode) }
  
  @testing = testmode
  @repo = nil
  
  define_invariants
  verify { @testing == testmode }
  check_invariants
end

Instance Attribute Details

#current_viewObject (readonly)

Returns the value of attribute current_view.



6
7
8
# File 'lib/scriptorium/api.rb', line 6

def current_view
  @current_view
end

#repoObject (readonly)

Returns the value of attribute repo.



6
7
8
# File 'lib/scriptorium/api.rb', line 6

def repo
  @repo
end

Instance Method Details

#apply_theme(theme) ⇒ Object



79
80
81
# File 'lib/scriptorium/api.rb', line 79

def apply_theme(theme)
  @repo.view.apply_theme(theme)
end

#create_draft(title: nil, body: nil, views: nil, tags: nil, blurb: nil) ⇒ Object



146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/scriptorium/api.rb', line 146

def create_draft(title: nil, body: nil, views: nil, tags: nil, blurb: nil)
  views ||= @repo.current_view&.name
  raise "No view specified and no current view set" if views.nil?
  
  @repo.create_draft(
    title: title,
    body: body,
    views: views,
    tags: tags,
    blurb: blurb
  )
end

#create_post(title, body, views: nil, tags: nil, blurb: nil) ⇒ Object

Post creation with convenience defaults



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# File 'lib/scriptorium/api.rb', line 107

def create_post(title, body, views: nil, tags: nil, blurb: nil)
  check_invariants
  assume { title.is_a?(String) }
  assume { body.is_a?(String) }
  assume { views.nil? || views.is_a?(String) || views.is_a?(Array) }
  assume { tags.nil? || tags.is_a?(String) || tags.is_a?(Array) }
  assume { blurb.nil? || blurb.is_a?(String) }
  assume { @repo.is_a?(Scriptorium::Repo) }
  
  views ||= @repo.current_view&.name
  raise "No view specified and no current view set" if views.nil?
  
  post = @repo.create_post(
    title: title,
    body: body,
    views: views,
    tags: tags,
    blurb: blurb
  )
  
  verify { post.is_a?(Scriptorium::Post) }
  check_invariants
  post
end

#create_repo(path) ⇒ Object

Raises:

  • (RepoDirAlreadyExists)


29
30
31
32
33
34
35
36
37
38
39
# File 'lib/scriptorium/api.rb', line 29

def create_repo(path)
  check_invariants
  assume { path.is_a?(String) && !path.empty? }
  
  raise RepoDirAlreadyExists if repo_exists?(path)
  Scriptorium::Repo.create(path)
  @repo = Scriptorium::Repo.open(path)
  
  verify { @repo.is_a?(Scriptorium::Repo) }
  check_invariants
end

#create_view(name, title, subtitle = "", theme: "standard") ⇒ Object

View management



52
53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/scriptorium/api.rb', line 52

def create_view(name, title, subtitle = "", theme: "standard")
  check_invariants
  assume { name.is_a?(String) }
  assume { title.is_a?(String) }
  assume { subtitle.is_a?(String) }
  assume { theme.is_a?(String) }
  assume { @repo.is_a?(Scriptorium::Repo) }
  
  @repo.create_view(name, title, subtitle, theme: theme)
  
  verify { @repo.is_a?(Scriptorium::Repo) }
  check_invariants
  self
end

#define_invariantsObject

Invariants



9
10
11
12
# File 'lib/scriptorium/api.rb', line 9

def define_invariants
  invariant { [true, false].include?(@testing) }
  invariant { @repo.nil? || @repo.is_a?(Scriptorium::Repo) }
end

#delete_draft(draft_path) ⇒ Object



526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
# File 'lib/scriptorium/api.rb', line 526

def delete_draft(draft_path)
  # Delete a draft file
  # draft_path: path to the draft file (e.g., from drafts() method)
  
  raise "Draft path cannot be nil" if draft_path.nil?
  raise "Draft path cannot be empty" if draft_path.to_s.strip.empty?
  
  # Ensure it's actually a draft file
  unless draft_path.to_s.end_with?('-draft.lt3')
    raise "Not a valid draft file: #{draft_path}"
  end
  
  # Ensure it exists
  unless File.exist?(draft_path)
    raise "Draft file not found: #{draft_path}"
  end
  
  # Delete the file
  File.delete(draft_path)
  true
end

#delete_post(id) ⇒ Object

Post management



235
236
237
238
239
240
241
242
243
244
# File 'lib/scriptorium/api.rb', line 235

def delete_post(id)
  post = @repo.post(id)
  old_path = @repo.root/:posts/post.num
  new_path = @repo.root/:posts/"_#{post.num}"
  FileUtils.mv(old_path, new_path)
  
  # Set the deleted flag in metadata
  post.meta["post.deleted"] = "true"
  post.
end

#draft(title: nil, body: nil, views: nil, tags: nil, blurb: nil) ⇒ Object

Draft management



133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/scriptorium/api.rb', line 133

def draft(title: nil, body: nil, views: nil, tags: nil, blurb: nil)
  views ||= @repo.current_view&.name
  raise "No view specified and no current view set" if views.nil?
  
  @repo.create_draft(
    title: title,
    body: body,
    views: views,
    tags: tags,
    blurb: blurb
  )
end

#draftsObject

Draft management



513
514
515
516
517
518
519
520
521
522
523
524
# File 'lib/scriptorium/api.rb', line 513

def drafts
  drafts_dir = @repo.root/:drafts
  return [] unless Dir.exist?(drafts_dir)
  
  draft_files = Dir.children(drafts_dir).select { |f| f.end_with?('-draft.lt3') }
  draft_files.map do |filename|
    path = drafts_dir/filename
    # Quick scan for title from the draft file
    title = extract_title_from_draft(path)
    { path: path, title: title }
  end
end

#edit_config(view = nil) ⇒ Object



402
403
404
405
406
# File 'lib/scriptorium/api.rb', line 402

def edit_config(view = nil)
  view ||= @repo.current_view&.name
  raise "No view specified and no current view set" if view.nil?
  edit_file("views/#{view}/config.txt")
end

#edit_deploy_configObject



419
420
421
# File 'lib/scriptorium/api.rb', line 419

def edit_deploy_config
  edit_file("config/deploy.txt")
end

#edit_file(path) ⇒ Object

File operations

Raises:

  • (CannotEditFilePathNil)


437
438
439
440
441
442
443
444
# File 'lib/scriptorium/api.rb', line 437

def edit_file(path)
  # Input validation
  raise CannotEditFilePathNil if path.nil?
  raise CannotEditFilePathEmpty if path.to_s.strip.empty?
  
  editor = ENV['EDITOR'] || 'vim'
  system!(editor, path)
end

#edit_layout(view = nil) ⇒ Object

Convenience file editing methods



396
397
398
399
400
# File 'lib/scriptorium/api.rb', line 396

def edit_layout(view = nil)
  view ||= @repo.current_view&.name
  raise "No view specified and no current view set" if view.nil?
  edit_file("views/#{view}/layout.txt")
end

#edit_post(post_id) ⇒ Object



423
424
425
426
427
428
429
430
431
432
433
# File 'lib/scriptorium/api.rb', line 423

def edit_post(post_id)
  post = @repo.post(post_id)
  source_path = "posts/#{post.num}/source.lt3"
  body_path = "posts/#{post.num}/body.html"
  
  if File.exist?(source_path)
    edit_file(source_path)
  else
    edit_file(body_path)
  end
end

#edit_repo_configObject



415
416
417
# File 'lib/scriptorium/api.rb', line 415

def edit_repo_config
  edit_file("config/repo.txt")
end

#edit_widget_data(view = nil, widget) ⇒ Object



408
409
410
411
412
413
# File 'lib/scriptorium/api.rb', line 408

def edit_widget_data(view = nil, widget)
  view ||= @repo.current_view&.name
  raise "No view specified and no current view set" if view.nil?
  raise "Widget name cannot be nil" if widget.nil?
  edit_file("views/#{view}/widgets/#{widget}/list.txt")
end

#finish_draft(draft_path) ⇒ Object



159
160
161
# File 'lib/scriptorium/api.rb', line 159

def finish_draft(draft_path)
  @repo.finish_draft(draft_path)
end

#generate_front_page(view = nil) ⇒ Object

Generation



164
165
166
167
168
169
# File 'lib/scriptorium/api.rb', line 164

def generate_front_page(view = nil)
  view ||= @repo.current_view&.name
  raise "No view specified and no current view set" if view.nil?
  
  @repo.generate_front_page(view)
end

#generate_post(post_id) ⇒ Object



178
179
180
181
182
183
184
185
186
187
188
189
190
191
# File 'lib/scriptorium/api.rb', line 178

def generate_post(post_id)
  # Check if the post directory exists first
  post_dir = @repo.root/:posts/d4(post_id)
  if Dir.exist?(post_dir)
    # Post directory exists, proceed with generation
    @repo.generate_post(post_id)
  else
    # Try to find the post through normal means
    post = @repo.post(post_id)
    raise "Post not found" if post.nil?
    
    @repo.generate_post(post_id)
  end
end

#generate_post_index(view = nil) ⇒ Object



171
172
173
174
175
176
# File 'lib/scriptorium/api.rb', line 171

def generate_post_index(view = nil)
  view ||= @repo.current_view&.name
  raise "No view specified and no current view set" if view.nil?
  
  @repo.generate_post_index(view)
end

#generate_view(view = nil) ⇒ Object

Generation



502
503
504
505
506
507
508
# File 'lib/scriptorium/api.rb', line 502

def generate_view(view = nil)
  view ||= @repo.current_view&.name
  raise "No view specified and no current view set" if view.nil?
  
  @repo.generate_front_page(view)
  true
end

#generate_widget(widget_name) ⇒ Object



363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
# File 'lib/scriptorium/api.rb', line 363

def generate_widget(widget_name)
  # Generate a specific widget for the current view
  # widget_name: string name of the widget (e.g., "links", "news")
  # Returns true on success, raises error on failure
  
  raise "No current view set" if @repo.current_view.nil?
  raise "Widget name cannot be nil" if widget_name.nil?
  raise "Widget name cannot be empty" if widget_name.to_s.strip.empty?
  
  # Validate widget name format
  unless widget_name.to_s.match?(/^[a-zA-Z0-9_]+$/)
    raise "Invalid widget name: #{widget_name} (must be alphanumeric and underscore only)"
  end
  
  # Convert to class name (capitalize first letter)
  widget_class_name = widget_name.to_s.capitalize
  
  # Try to find the widget class
  begin
    widget_class = eval("Scriptorium::Widget::#{widget_class_name}")
  rescue NameError
    raise "Widget class not found: Scriptorium::Widget::#{widget_class_name}"
  end
  
  # Create widget instance and generate
  widget = widget_class.new(@repo, @repo.current_view)
  widget.generate
  
  true
end

#get_published_posts(view = nil) ⇒ Object



214
215
216
217
# File 'lib/scriptorium/api.rb', line 214

def get_published_posts(view = nil)
  view ||= @repo.current_view&.name
  @repo.get_published_posts(view)
end

#link_post(id, view = nil) ⇒ Object



280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
# File 'lib/scriptorium/api.rb', line 280

def link_post(id, view = nil)
  # Add post to a specific view (or current view if none specified)
  view ||= @repo.current_view&.name
  raise "No view specified and no current view set" if view.nil?
  
  post = @repo.post(id)
  raise "Post not found" if post.nil?
  
  current_views = post.views.strip.split(/\s+/)
  new_views = current_views.include?(view) ? current_views : current_views + [view]
  result = update_post(id, {views: new_views})
  
  @repo.generate_post(id) if result
  
  result
end

#lookup_view(view_name) ⇒ Object



97
98
99
# File 'lib/scriptorium/api.rb', line 97

def lookup_view(target)
  @repo&.lookup_view(target)
end

#open_repo(path) ⇒ Object



41
42
43
44
45
46
47
48
49
# File 'lib/scriptorium/api.rb', line 41

def open_repo(path)
  check_invariants
  assume { path.is_a?(String) && !path.empty? }
  
  @repo = Scriptorium::Repo.open(path)
  
  verify { @repo.is_a?(Scriptorium::Repo) }
  check_invariants
end

#post(id) ⇒ Object



230
231
232
# File 'lib/scriptorium/api.rb', line 230

def post(id)
  @repo.post(id)
end

#post_add_tag(id, tag) ⇒ Object



309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/scriptorium/api.rb', line 309

def post_add_tag(id, tag)
  # Add a tag to a post
  post = @repo.post(id)
  raise "Post not found" if post.nil?
  
  # Get current tags from metadata (split comma-separated string into array)
  current_tags = post.tags.strip.split(/,\s*/)
  
  # Add the tag (avoid duplicates)
  new_tags = current_tags.include?(tag) ? current_tags : current_tags + [tag]
  
  # Update the post with new tags list
  result = update_post(id, {tags: new_tags})
  
  # Regenerate the post to update metadata
  @repo.generate_post(id) if result
  
  result
end

#post_add_view(id, view) ⇒ Object



297
298
299
300
301
# File 'lib/scriptorium/api.rb', line 297

def post_add_view(id, view)
  # Add a view to a post (view can be string or View object)
  view_name = view.is_a?(String) ? view : view.name
  link_post(id, view_name)
end

#post_attrs(post_id, *keys) ⇒ Object



225
226
227
228
# File 'lib/scriptorium/api.rb', line 225

def post_attrs(post_id, *keys)
  post = post_id.is_a?(Integer) ? @repo.post(post_id) : post_id
  post.attrs(*keys)
end

#post_published?(num) ⇒ Boolean

Returns:

  • (Boolean)


210
211
212
# File 'lib/scriptorium/api.rb', line 210

def (num)
  @repo.(num)
end

#post_remove_tag(id, tag) ⇒ Object



329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
# File 'lib/scriptorium/api.rb', line 329

def post_remove_tag(id, tag)
  # Remove a tag from a post
  post = @repo.post(id)
  raise "Post not found" if post.nil?
  
  # Get current tags from metadata (split comma-separated string into array)
  current_tags = post.tags.strip.split(/,\s*/)
  
  # Remove the tag
  new_tags = current_tags - [tag]
  
  # Update the post with new tags list
  result = update_post(id, {tags: new_tags})
  
  # Regenerate the post to update metadata
  @repo.generate_post(id) if result
  
  result
end

#post_remove_view(id, view) ⇒ Object



303
304
305
306
307
# File 'lib/scriptorium/api.rb', line 303

def post_remove_view(id, view)
  # Remove a view from a post (view can be string or View object)
  view_name = view.is_a?(String) ? view : view.name
  unlink_post(id, view_name)
end

#posts(view = nil) ⇒ Object

Post retrieval



220
221
222
223
# File 'lib/scriptorium/api.rb', line 220

def posts(view = nil)
  view ||= @repo.current_view&.name
  @repo.all_posts(view)
end

#publish_post(num) ⇒ Object

Publication system



198
199
200
201
202
203
204
205
206
207
208
# File 'lib/scriptorium/api.rb', line 198

def publish_post(num)
  check_invariants
  assume { num.is_a?(Integer) }
  assume { @repo.is_a?(Scriptorium::Repo) }
  
  post = @repo.publish_post(num)
  
  verify { post.is_a?(Scriptorium::Post) }
  check_invariants
  post
end

#repo_exists?(path) ⇒ Boolean

Returns:

  • (Boolean)


25
26
27
# File 'lib/scriptorium/api.rb', line 25

def repo_exists?(path)
  Dir.exist?(path)
end

#rootObject



71
72
73
# File 'lib/scriptorium/api.rb', line 71

def root
  @repo.root
end

#search_posts(**criteria) ⇒ Object



456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
# File 'lib/scriptorium/api.rb', line 456

def search_posts(**criteria)
  # Search posts using keyword criteria
  # criteria: hash of {field: pattern} where field is :title, :body, :tags, :blurb
  # pattern: string (exact match) or regex (pattern match)
  # Example: api.search_posts(title: /Ruby/, tags: "scriptorium")
  
  all_posts = @repo.all_posts
  matching_posts = []
  
  all_posts.each do |post|
    matches_all_criteria = true
    
    criteria.each do |field, pattern|
      # Get the field value from the post
      field_value = case field
      when :title
        post.title
      when :body
        # Read the body from the source file
        body_file = post.dir/"body.html"
        File.exist?(body_file) ? read_file(body_file) : ""
      when :tags
        post.tags
            when :blurb
      post.blurb
      else
        raise "Unknown search field: #{field}"
      end
      
      # Check if the pattern matches
      if pattern.is_a?(Regexp)
        matches_all_criteria = false unless field_value.match?(pattern)
      else
        matches_all_criteria = false unless field_value.include?(pattern.to_s)
      end
      
      break unless matches_all_criteria
    end
    
    matching_posts << post if matches_all_criteria
  end
  
  matching_posts
end

#select_posts(&block) ⇒ Object

Post selection and search



447
448
449
450
451
452
453
454
# File 'lib/scriptorium/api.rb', line 447

def select_posts(&block)
  # Filter posts using a block
  # Returns array of posts that match the block condition
  # Example: api.select_posts { |post| post.views.include?("alpha") }
  
  all_posts = @repo.all_posts
  all_posts.select(&block)
end

#themes_availableObject

Theme management



350
351
352
353
354
# File 'lib/scriptorium/api.rb', line 350

def themes_available
  themes_dir = @repo.root/:themes
  return [] unless Dir.exist?(themes_dir)
  Dir.children(themes_dir).select { |d| Dir.exist?(themes_dir/d) }
end

#undelete_post(id) ⇒ Object



246
247
248
249
250
251
252
253
254
255
# File 'lib/scriptorium/api.rb', line 246

def undelete_post(id)
  post = @repo.post(id)
  old_path = @repo.root/:posts/"_#{post.num}"
  new_path = @repo.root/:posts/post.num
  FileUtils.mv(old_path, new_path)
  
  # Clear the deleted flag in metadata
  post.meta["post.deleted"] = "false"
  post.
end

#unlink_post(id, view = nil) ⇒ Object



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/api.rb', line 257

def unlink_post(id, view = nil)
  # Remove post from a specific view (or current view if none specified)
  view ||= @repo.current_view&.name
  raise "No view specified and no current view set" if view.nil?
  
  post = @repo.post(id)
  raise "Post not found" if post.nil?
  
  # Get current views from metadata (split string into array)
  current_views = post.views.strip.split(/\s+/)
  
  # Remove the specified view
  new_views = current_views - [view]
  
  # Update the post with new views list
  result = update_post(id, {views: new_views})
  
  # Regenerate the post to update metadata
  @repo.generate_post(id) if result
  
  result
end

#update_post(id, fields) ⇒ Object



561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
# File 'lib/scriptorium/api.rb', line 561

def update_post(id, fields)
  # Update fields in the post's source.lt3 file
  # fields: hash of {field: value} where field is livetext dotcmd (e.g., :views, :title, :tags)
  # value: string or array of strings
  
  post = @repo.post(id)
  source_file = post.dir/"source.lt3"
  return false unless File.exist?(source_file)
  
  # Read the file
  lines = read_file(source_file, lines: true, chomp: false)
  updated = false
  
  # Process each field
  fields.each do |field, value|
    # Convert value to array
    value_array = Array(value)
    
    # Handle different field types
    case field
    when :tags
      # Tags should be comma-separated
      new_value = value_array.join(", ")
    else
      # Other fields (views, etc.) should be space-separated
      new_value = value_array.join(' ')
    end
    
    lines.map! do |line|
      if line.strip.start_with?(".#{field}")
        # Preserve trailing comments
        comment_match = line.match(/(\s+#.*)$/)
        comment = comment_match ? comment_match[1] : ""
        
        # Add change comment
        timestamp = Time.now.strftime("%Y/%m/%d %H:%M:%S")
        change_comment = " # updated #{field} #{timestamp}"
        
        updated = true
        ".#{field} #{new_value}#{comment}#{change_comment}\n"
      else
        line
      end
    end
  end
  
  return false unless updated
  
  # Write the updated file
  write_file(source_file, lines.join)
  true
end

#versionObject



75
76
77
# File 'lib/scriptorium/api.rb', line 75

def version
  Scriptorium::VERSION
end

#view(name = nil) ⇒ Object

Post management



84
85
86
87
88
89
90
91
# File 'lib/scriptorium/api.rb', line 84

def view(name = nil)
  if name.nil?
    @repo.current_view
  else
    result = @repo.view(name)
    result
  end
end

#viewsObject



93
94
95
# File 'lib/scriptorium/api.rb', line 93

def views
  @repo&.views || []
end

#views_for(post_or_id) ⇒ Object



101
102
103
104
# File 'lib/scriptorium/api.rb', line 101

def views_for(post_or_id)
  post = post_or_id.is_a?(Integer) ? @repo.post(post_or_id) : post_or_id
  post.views&.split(/\s+/) || []
end

#widgets_availableObject

Widget management



357
358
359
360
361
# File 'lib/scriptorium/api.rb', line 357

def widgets_available
  widgets_file = @repo.root/:config/"widgets.txt"
  return [] unless File.exist?(widgets_file)
  read_file(widgets_file, lines: true, chomp: true)
end