Class: MarkdownExec::FCB

Inherits:
Object show all
Defined in:
lib/fcb.rb

Overview

Fenced Code Block (FCB)

This class represents a fenced code block in a markdown document. It allows for setting and getting attributes related to the code block, such as body, call, headings, and more.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = {}) ⇒ FCB

Returns a new instance of FCB.



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/fcb.rb', line 56

def initialize(options = {})
  @attrs = {
    block: nil,
    body: nil,
    call: nil,
    dname: nil,
    headings: [],
    id: object_id,
    indent: '',
    name: nil,
    nickname: nil,
    oname: nil,
    random: Random.new.rand,
    reqs: [],
    shell: '',
    start_line: nil,
    text: nil, # displayable in menu
    title: '',
    type: ''
  }.merge(options)
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

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

:reek:ManualDispatch



374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# File 'lib/fcb.rb', line 374

def method_missing(method, *args, &block)
  method_name = method.to_s
  if @attrs.respond_to?(method_name)
    @attrs.send(method_name, *args, &block)
  elsif method_name[-1] == '='
    @attrs[method_name.chop.to_sym] = args[0]
  else
    @attrs[method_name.to_sym]
  end
rescue StandardError => err
  warn("ERROR ** FCB.method_missing(method: #{method_name}," \
       " *args: #{args.inspect}, &block)")
  warn err.inspect
  warn(caller[0..4])
  raise err
end

Class Method Details

.act_source(export) ⇒ Object



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/fcb.rb', line 256

def self.act_source(export)
  # If `false`, the UX block is not activated.
  # If one of `:allow`, `:echo`, `:edit`, or `:exec` is specified,
  # the value is calculated or the user is prompted.
  # If not present, the default value is `:edit`.
  if export.act.nil?
    export.act = if export.init.to_s == 'false'
                   # if export.allow.present?
                   if FCB.is_allow?(export)
                     UxActSource::ALLOW
                   # elsif export.echo.present?
                   elsif FCB.is_echo?(export)
                     UxActSource::ECHO
                   # elsif export.edit.present?
                   elsif FCB.is_edit?(export)
                     UxActSource::EDIT
                   # elsif export.exec.present?
                   elsif FCB.is_exec?(export)
                     UxActSource::EXEC
                   else
                     UxActSource::EDIT
                   end
                 elsif FCB.is_allow?(export)
                   UxActSource::ALLOW
                 else
                   UxActSource::EDIT
                 end
  end

  export.act
end

.format_multiline_body_as_title(body_lines) ⇒ String

Formats multiline body content as a title string. indents all but first line with two spaces so it displays correctly in menu.

Parameters:

  • body_lines (Array<String>)

    The lines of body content.

Returns:

  • (String)

    Formatted title.



218
219
220
221
222
# File 'lib/fcb.rb', line 218

def self.format_multiline_body_as_title(body_lines)
  body_lines.map.with_index do |line, index|
    index.zero? ? line : "  #{line}"
  end.join("\n")
end

.init_source(export) ⇒ Object



288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/fcb.rb', line 288

def self.init_source(export)
  # If `false`, there is no initial value set.
  # If a string, it is the initial value of the object variable.
  # Otherwise, if one of `:allow`, `:echo`, or `:exec` is specified,
  # the value is the output of the `echo` or `exec` evaluation
  # or the first allowed value.
  # If not present, the default value is whichever of
  # `:allow`, `:default`, `:echo`, or `:exec` is present.
  if export.init.nil?
    export.init = case
                  when FCB.is_allow?(export)
                    UxActSource::ALLOW
                  when export.default.present?
                    UxActSource::DEFAULT
                  # when export.echo.present?
                  when FCB.is_echo?(export)
                    UxActSource::ECHO
                  # when export.exec.present?
                  when FCB.is_exec?(export)
                    UxActSource::EXEC
                  else
                    UxActSource::FALSE
                  end
  end

  export.init
end

.is_allow?(export) ⇒ Boolean

Returns:

  • (Boolean)


224
225
226
# File 'lib/fcb.rb', line 224

def self.is_allow?(export)
  export&.allow&.present?
end

.is_echo?(export) ⇒ Boolean

Returns:

  • (Boolean)


232
233
234
# File 'lib/fcb.rb', line 232

def self.is_echo?(export)
  export&.echo&.present?
end

.is_edit?(export) ⇒ Boolean

Returns:

  • (Boolean)


240
241
242
# File 'lib/fcb.rb', line 240

def self.is_edit?(export)
  export&.edit&.present?
end

.is_exec?(export) ⇒ Boolean

Returns:

  • (Boolean)


248
249
250
# File 'lib/fcb.rb', line 248

def self.is_exec?(export)
  export&.exec&.present?
end

.pub_name(attrs, **kwargs) ⇒ Object



86
87
88
89
# File 'lib/fcb.rb', line 86

def self.pub_name(attrs, **kwargs)
  full = attrs.fetch(:nickname, nil) || attrs.fetch(:oname, nil)
  full&.to_s&.pub_name(**kwargs)
end

Instance Method Details

#append_block_line(line) ⇒ Object



78
79
80
# File 'lib/fcb.rb', line 78

def append_block_line(line)
  @attrs[:block].push line
end

#code_name_exp?(regexp) ⇒ Boolean

Returns:

  • (Boolean)


95
96
97
# File 'lib/fcb.rb', line 95

def code_name_exp?(regexp)
  Regexp.new(regexp) =~ @attrs[:oname]
end

#code_name_included?(*names) ⇒ Boolean

Returns:

  • (Boolean)


91
92
93
# File 'lib/fcb.rb', line 91

def code_name_included?(*names)
  names.include?(@attrs[:oname])
end

#delete_key(key) ⇒ Object



99
100
101
# File 'lib/fcb.rb', line 99

def delete_key(key)
  @attrs.delete(key)
end

#delete_matching_name!(dependencies) ⇒ Object

Removes and returns the first matching name from dependencies collection Checks nickname, oname, pub_name and s2title 2024-08-04 match oname for long block names 2024-08-04 match nickname may not exist if block name is duplicated



108
109
110
111
112
113
114
115
# File 'lib/fcb.rb', line 108

def delete_matching_name!(dependencies)
  dependencies.delete(@attrs[:id]) ||
    dependencies.delete(@attrs[:dname]) ||
    dependencies.delete(@attrs[:nickname]) ||
    dependencies.delete(@attrs[:oname]) ||
    dependencies.delete(@attrs.pub_name) ||
    dependencies.delete(@attrs[:s2title])
end

#derive_title_from_bodyString

Derives a title from the body of an FCB object.

Parameters:

  • fcb (Object)

    The FCB object whose title is to be derived.

Returns:

  • (String)

    The derived title.



120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/fcb.rb', line 120

def derive_title_from_body
  unless (body_content = @attrs[:body])
    # empty body -> empty title
    @attrs[:title] = ''
    return
  end

  # body -> title
  @attrs[:title] = if body_content.count == 1
                     body_content.first
                   else
                     FCB.format_multiline_body_as_title(body_content)
                   end
end

#expand_variables_in_attributes!(pattern, replacements) ⇒ void

This method returns an undefined value.

Expand variables in attributes



480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
# File 'lib/fcb.rb', line 480

def expand_variables_in_attributes!(pattern, replacements)
  @attrs[:raw_dname] ||= @attrs[:dname]
  @attrs[:dname] = @attrs[:dname]&.gsub(pattern) do |match|
    replacements[match]
  end

  @attrs[:raw_s0printable] ||= @attrs[:s0printable]
  @attrs[:s0printable] = @attrs[:s0printable]&.gsub(pattern) do |match|
    replacements[match]
  end

  @attrs[:raw_s1decorated] ||= @attrs[:s1decorated]
  @attrs[:s1decorated] = @attrs[:s1decorated]&.gsub(pattern) do |match|
    replacements[match]
  end

  # Replace variables in each line of `body` if `body` is present
  return unless @attrs[:body]

  # save body for YAML and re-interpretation
  @attrs[:raw_body] ||= @attrs[:body]
  @attrs[:body] = @attrs[:body]&.map do |line|
    if line.empty?
      line
    else
      line.gsub(pattern) { |match| replacements[match] }
    end
  end
end

#for_menu!(appopts:, block_calls_scan:, block_name_match:, block_name_nick_match:, id: '', menu_format:, prompt:, table_center:) ⇒ Object

Processes a block to generate its summary, modifying its attributes

based on various matching criteria.

It handles special formatting for bash blocks, extracting and setting

properties like call, stdin, stdout, and dname.

Parameters:

  • fcb (Object)

    An object representing a functional code block.

Returns:

  • (Object)

    The modified functional code block with updated summary attributes.



143
144
145
146
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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/fcb.rb', line 143

def for_menu!(
  appopts:,
  block_calls_scan:,
  block_name_match:,
  block_name_nick_match:,
  id: '',
  menu_format:,
  prompt:,
  table_center:
)
  call = @attrs[:call] = @attrs[:start_line]&.match(
    Regexp.new(block_calls_scan)
  )&.fetch(1, nil)
  titlexcall = call ? @attrs[:title].sub("%#{call}", '') : @attrs[:title]

  oname = if is_split?
            @attrs[:text]
          elsif block_name_nick_match.present? &&
                @attrs[:oname] =~ Regexp.new(block_name_nick_match)
            @attrs[:nickname] = $~[0]
            derive_title_from_body
          else
            bm = NamedCaptureExtractor.extract_named_groups(
              titlexcall,
              block_name_match
            )
            bm && bm[1] ? bm[:title] : titlexcall
          end
  @attrs[:title] = @attrs[:oname] = oname
  @attrs[:id] = id

  if @attrs[:type] == BlockType::UX
    begin
      case data = YAML.load(@attrs[:body].join("\n"))
      when Hash
        export = parse_yaml_of_ux_block(
          data,
          prompt: prompt
        )

        if !export.menu_format || export.menu_format.empty?
          format_symbol = option_to_format_ux_block(export)
          export.menu_format = appopts[format_symbol]
          if !export.menu_format || export.menu_format.empty?
            export.menu_format = appopts[:menu_ux_row_format]
          end
        end
        @attrs[:oname] = oname = format(export.menu_format, export.to_h)

        @attrs[:center] = table_center
        @attrs[:readonly] = export.readonly
      else
        # triggered by an empty or non-YAML block
        return NullResult.new(message: 'Invalid YAML', data: data)
      end
    rescue StandardError
      wwe 'Error processing block for menu', 'body:', @attrs[:body],
          'data', data, 'export', export
    end
  end

  @attrs[:dname] = HashDelegator.indent_all_lines(
    # yield the text and option name for the color
    (yield oname, option_to_decorate_ux_block),
    @attrs[:indent]
  )

  SuccessResult.instance
end

#is_allow?Boolean

Returns:

  • (Boolean)


228
229
230
# File 'lib/fcb.rb', line 228

def is_allow?
  FCB.is_allow?(export)
end

#is_dependency_of?(dependency_names) ⇒ Boolean

:reek:ManualDispatch 2024-08-04 match nickname

Returns:

  • (Boolean)


318
319
320
321
322
323
324
325
# File 'lib/fcb.rb', line 318

def is_dependency_of?(dependency_names)
  dependency_names.include?(@attrs[:id]) ||
    dependency_names.include?(@attrs[:dname]) ||
    dependency_names.include?(@attrs[:nickname]) ||
    dependency_names.include?(@attrs[:oname]) ||
    dependency_names.include?(@attrs.pub_name) ||
    dependency_names.include?(@attrs[:s2title])
end

#is_disabled?Boolean

Returns:

  • (Boolean)


327
328
329
# File 'lib/fcb.rb', line 327

def is_disabled?
  @attrs[:disabled] == TtyMenu::DISABLE
end

#is_echo?Boolean

Returns:

  • (Boolean)


236
237
238
# File 'lib/fcb.rb', line 236

def is_echo?
  FCB.is_echo?(export)
end

#is_edit?Boolean

Returns:

  • (Boolean)


244
245
246
# File 'lib/fcb.rb', line 244

def is_edit?
  FCB.is_edit?(export)
end

#is_enabled?Boolean

Returns:

  • (Boolean)


331
332
333
# File 'lib/fcb.rb', line 331

def is_enabled?
  !is_disabled?
end

#is_exec?Boolean

Returns:

  • (Boolean)


252
253
254
# File 'lib/fcb.rb', line 252

def is_exec?
  FCB.is_exec?(export)
end

#is_named?(name) ⇒ Boolean

Returns:

  • (Boolean)


335
336
337
338
339
340
341
342
343
344
345
346
# File 'lib/fcb.rb', line 335

def is_named?(name)
  if /^ItrBlk/.match(name)
    @attrs[:id] == name
  else
    @attrs[:id] == name ||
      @attrs[:dname] == name ||
      @attrs[:nickname] == name ||
      @attrs[:oname] == name ||
      @attrs.pub_name == name ||
      @attrs[:s2title] == name
  end
end

#is_split?Boolean

true if this is a line split block

Returns:

  • (Boolean)


349
350
351
# File 'lib/fcb.rb', line 349

def is_split?
  is_split_first? || is_split_rest?
end

#is_split_displayed?(opts) ⇒ Boolean

true if this block displays its split body names and nicknames are displayed instead of the body ux blocks display a single line for the named variable split blocks are: opts, shell, vars

Returns:

  • (Boolean)


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

def is_split_displayed?(opts)
  @attrs[:type] != BlockType::UX &&
    !(@attrs[:start_line] =~ Regexp.new(opts[:block_name_nick_match]) ||
       @attrs[:start_line] =~ Regexp.new(opts[:block_name_match]))
end

#is_split_first?Boolean

true if this is the first line in a split block

Returns:

  • (Boolean)


364
365
366
# File 'lib/fcb.rb', line 364

def is_split_first?
  @attrs.fetch(:is_split_first, false)
end

#is_split_rest?Boolean

true if this is the second or later line in a split block

Returns:

  • (Boolean)


369
370
371
# File 'lib/fcb.rb', line 369

def is_split_rest?
  @attrs.fetch(:is_split_rest, false)
end

#name_in_menu!(indented_multi_line) ⇒ Object



391
392
393
394
395
396
397
398
399
400
# File 'lib/fcb.rb', line 391

def name_in_menu!(indented_multi_line)
  # Indent has been extracted from the first line,
  # remove indent from the remaining lines.
  @attrs[:dname] =
    if @attrs[:indent].empty?
      indented_multi_line
    else
      indented_multi_line.gsub("\n#{@attrs[:indent]}", "\n")
    end
end

#option_to_decorate_ux_blockObject

calc the decoration sybol for the current block



403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# File 'lib/fcb.rb', line 403

def option_to_decorate_ux_block
  symbol_or_hash = BLOCK_TYPE_COLOR_OPTIONS[@attrs[:type]] || BLOCK_TYPE_COLOR_OPTIONS[true]
  if @attrs[:type] == BlockType::UX
    # only UX blocks accept a symbol or a hash
    if symbol_or_hash.is_a? Hash
      # default to the first symbol
      symbol = symbol_or_hash.first.last
      symbol_or_hash.each_key do |key|
        if key == true
          symbol = symbol_or_hash[key]
          break
        elsif symbol_or_hash[key].present? && send(key)
          symbol = symbol_or_hash[key]
          break
        end
      end
      symbol
    else
      # only symbol
      symbol_or_hash
    end
  else
    # only symbol
    symbol_or_hash
  end
end

#option_to_format_ux_block(export) ⇒ Object



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

def option_to_format_ux_block(export)
  if export.readonly
    :menu_ux_row_format_readonly
  else
    case FCB.act_source(export)
    when UxActSource::ALLOW
      :menu_ux_row_format_allow
    when UxActSource::ECHO
      :menu_ux_row_format_echo
    when UxActSource::EDIT
      :menu_ux_row_format_edit
    when UxActSource::EXEC
      :menu_ux_row_format_exec
    else
      # this UX block does not have a format, treat as editable
      :menu_ux_row_format_edit
    end
  end
end

#pub_name(**kwargs) ⇒ Object



82
83
84
# File 'lib/fcb.rb', line 82

def pub_name(**kwargs)
  self.class.pub_name(@attrs, **kwargs)
end

#respond_to_missing?(method_name, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


450
451
452
# File 'lib/fcb.rb', line 450

def respond_to_missing?(method_name, include_private = false)
  @attrs.key?(method_name.to_sym) || super
end

#shellObject



454
455
456
# File 'lib/fcb.rb', line 454

def shell
  @attrs[:shell]
end

#shell=(value) ⇒ Object



458
459
460
# File 'lib/fcb.rb', line 458

def shell=(value)
  @attrs[:shell] = value
end

#to_hObject



470
471
472
# File 'lib/fcb.rb', line 470

def to_h
  @attrs.to_h
end

#to_yamlObject



474
475
476
# File 'lib/fcb.rb', line 474

def to_yaml
  @attrs.to_yaml
end

#typeObject



462
463
464
# File 'lib/fcb.rb', line 462

def type
  @attrs[:type]
end

#type=(value) ⇒ Object



466
467
468
# File 'lib/fcb.rb', line 466

def type=(value)
  @attrs[:type] = value
end