Class: MarkdownExec::MDoc

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

Overview

MDoc represents an imported markdown document.

It provides methods to extract and manipulate specific sections of the document, such as code blocks. It also supports recursion to fetch related or dependent blocks.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(table = []) ⇒ MDoc

Initializes an instance of MDoc with the given table of markdown sections.

Parameters:

  • (defaults to: [])

    An array of hashes representing markdown sections.



67
68
69
# File 'lib/mdoc.rb', line 67

def initialize(table = [])
  @table = table
end

Instance Attribute Details

#tableObject (readonly)

Returns the value of attribute table.



61
62
63
# File 'lib/mdoc.rb', line 61

def table
  @table
end

Instance Method Details

#code_for_fcb_body_into_var_or_file(fcb) ⇒ String

Generates a shell command to copy the block’s body to either a shell variable or a file.

Parameters:

  • A hash containing information about the script block’s stdout and body. @option fcb [Hash] :stdout A hash specifying the stdout details.

    @option stdout [Boolean] :type Indicates whether to export to a variable (true) or to write to a file (false).
    @option stdout [String] :name The name of the variable or file to which the body will be output.
    

    @option fcb [Array<String>] :body An array of strings representing the lines of the script block’s body.

Returns:

  • A string containing the shell command to redirect the script block’s body. If stdout is true, the command will export the body to a shell variable. If stdout is false, the command will write the body to a file.



118
119
120
121
122
123
124
125
126
127
128
# File 'lib/mdoc.rb', line 118

def code_for_fcb_body_into_var_or_file(fcb)
  stdout = fcb[:stdout]
  body = fcb.body.join("\n")
  if stdout[:type]
    %(export #{stdout[:name]}=$(cat <<"EOF"\n#{body}\nEOF\n))
  else
    "cat > '#{stdout[:name]}' <<\"EOF\"\n" \
      "#{body}\n" \
      "EOF\n"
  end
end

#collect_block_dependencies(anyname:) ⇒ Array<Hash>

Retrieves code blocks that are required by a specified code block.

Parameters:

  • The name of the code block to start the retrieval from.

Returns:

  • An array of code blocks required by the specified code block.



135
136
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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/mdoc.rb', line 135

def collect_block_dependencies(anyname:)
  name_block = get_block_by_anyname(anyname)
  if name_block.nil? || name_block.keys.empty?
    raise "Named code block `#{anyname}` not found. (@#{__LINE__})"
  end

  nickname = name_block.pub_name
  ref = name_block.id
  dependencies = collect_dependencies(pubname: ref)
  wwt :dependencies, 'dependencies.count:', dependencies.count

  all_dependency_names =
    collect_unique_names(dependencies).push(ref).uniq
  wwt :dependencies, 'all_dependency_names.count:',
      all_dependency_names.count

  # select blocks in order of appearance in source documents
  #
  blocks = table_not_split.select do |fcb|
    fcb.is_dependency_of?(all_dependency_names)
  end
  wwt :blocks, 'blocks.count:', blocks.count

  ## add cann key to blocks, calc unmet_dependencies
  #
  unmet_dependencies = all_dependency_names.dup
  blocks = blocks.map do |fcb|
    fcb.delete_matching_name!(unmet_dependencies)
    if (call = fcb.call)
      fcb1 = get_block_by_anyname("[#{call.match(/^%\((\S+) |\)/)[1]}]")
      fcb1.cann = call
      [fcb1]
    else
      []
    end + [fcb]
  end.flatten(1)
  wwt :unmet_dependencies, 'unmet_dependencies.count:',
      unmet_dependencies.count
  wwt :dependencies, 'dependencies.keys:', dependencies.keys

  { all_dependency_names: all_dependency_names,
    blocks: blocks,
    dependencies: dependencies,
    unmet_dependencies: unmet_dependencies }
rescue StandardError
  wwe $!
end

#collect_dependencies(block: nil, memo: {}, pubname: nil) ⇒ Hash

Recursively collects dependencies of a given source.

Parameters:

  • The name of the initial source block.

  • (defaults to: {})

    A memoization hash to store resolved dependencies.

Returns:

  • A hash mapping sources to their respective dependencies.



555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
# File 'lib/mdoc.rb', line 555

def collect_dependencies(block: nil, memo: {}, pubname: nil)
  if block.nil?
    return memo unless pubname

    blocks = get_blocks_by_anyname(pubname)
    if blocks.empty?
      raise "Named code block `#{pubname}` not found. (@#{__LINE__})"
    end
  else
    blocks = [block]
  end
  blocks.each do |block|
    memo[block.id] = []
  end
  return memo unless blocks.count.positive?

  required_blocks = blocks.map(&:reqs).flatten(1)
  return memo unless required_blocks.count.positive?

  blocks.each do |block|
    memo[block.id] = required_blocks
  end

  required_blocks.each do |req|
    collect_dependencies(pubname: req, memo: memo)
  end

  memo
end

#collect_recursively_required_code(anyname:, block_source:, label_body: true, label_format_above: nil, label_format_below: nil, context_code: []) ⇒ OpenStruct

Collects recursively required code blocks and returns them as an array of strings.

Parameters:

  • The name of the code block to start the collection from.

Returns:



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# File 'lib/mdoc.rb', line 188

def collect_recursively_required_code(
  anyname:, block_source:,
  label_body: true, label_format_above: nil, label_format_below: nil,
  context_code: []
)
  raise 'unexpected label_body' unless label_body

  block_search = collect_block_dependencies(anyname: anyname)
  if block_search[:blocks]
    blocks = collect_wrapped_blocks(block_search[:blocks])
    # !!t blocks.count

    context_transient_codes = blocks.map do |fcb|
      process_block_to_code(
        fcb, block_source,
        label_body, label_format_above, label_format_below,
        context_code: context_code
      )
    end.tap { ww anyname, _1 }

    block_search.merge(
      {
        block_names: blocks.map(&:pub_name),
        context_code:
          context_transient_codes.map(&:context_code).compact.flatten(1).compact,
        transient_code:
          context_transient_codes.map(&:transient_code).compact.flatten(1).compact
      }
    )
  else
    block_search.merge(
      { block_names: [], context_code: [], transient_code: [] }
    )
  end.tap { wwr _1 }
rescue StandardError
  error_handler('collect_recursively_required_code')
end

#collect_unique_names(hash) ⇒ Object



226
227
228
# File 'lib/mdoc.rb', line 226

def collect_unique_names(hash)
  hash.values.flatten.uniq
end

#collect_wrapped_blocks(blocks) ⇒ Array<Hash>

Retrieves code blocks that are wrapped wraps are applied from left to right e.g. w1 w2 => w1-before w2-before w1 w2 w2-after w1-after

Returns:

  • An array of code blocks required by the specified code blocks.



236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/mdoc.rb', line 236

def collect_wrapped_blocks(blocks)
  blocks.map do |fcb|
    next if fcb.is_split_rest?

    (fcb[:wraps] || []).map do |wrap|
      wrap_before = wrap.sub('}', '-before}') ### hardcoded wrap name
      table_not_split.select { |fcb|
        fcb.code_name_included?(wrap_before, wrap)
      }
    end.flatten(1) +
      [fcb] +
      (fcb[:wraps] || []).reverse.map do |wrap|
        wrap_after = wrap.sub('}', '-after}') ### hardcoded wrap name
        table_not_split.select { |fcb| fcb.code_name_included?(wrap_after) }
      end.flatten(1)
  end.flatten(1).compact
rescue StandardError
  wwe $!
end

#error_handler(name = '', opts = {}) ⇒ Object



256
257
258
259
260
261
# File 'lib/mdoc.rb', line 256

def error_handler(name = '', opts = {})
  Exceptions.error_handler(
    "MDoc.#{name} -- #{$!}",
    opts
  )
end

#fcbs_per_options(opts = {}) ⇒ Array<Hash>

Retrieves code blocks based on the provided options.

Parameters:

  • (defaults to: {})

    The options used for filtering code blocks.

Returns:

  • An array of code blocks that match the options.



268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
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
315
316
317
318
319
320
321
322
# File 'lib/mdoc.rb', line 268

def fcbs_per_options(opts = {})
  options = opts.merge(block_name_hide_custom_match: nil)
  selrows = @table.select do |fcb_title_groups|
    Filter.fcb_select? options, fcb_title_groups
  end

  ### hide rows correctly

  unless opts[:menu_include_imported_blocks]
    selrows = selrows.reject do |fcb|
      fcb.fetch(:depth, 0).positive?
    end
  end

  if opts[:hide_blocks_by_name]
    selrows = selrows.reject do |fcb|
      hide_menu_block_on_name(opts, fcb)
    end
  end

  collapser = Collapser.new(
    options: opts,
    compress_ids: opts[:compressed_ids] || {},
    expand_ids: opts[:expanded_ids] || {}
  )
  selrows = collapser.reject(
    selrows,
    initialize: opts[:compressed_ids].nil?
  ) do |fcb, _hide, _collapsed_level|
    # update fcb per state
    next unless fcb.collapsible

    fcb.s1decorated = fcb.s1decorated + ' ' +
                      (if fcb.collapse
                         opts[:menu_collapsible_symbol_collapsed]
                       else
                         opts[:menu_collapsible_symbol_expanded]
                       end)
  end
  opts[:compressed_ids] = collapser.compress_ids
  opts[:expanded_ids] = collapser.expand_ids

  # remove
  # . empty chrome between code; edges are same as blanks
  #
  select_elements_with_neighbor_conditions(selrows) do |prev_element, current, next_element|
    !(current[:chrome] && !current.oname.present?) ||
      !(!prev_element.nil? &&
        prev_element.shell.present? &&
        !next_element.nil? &&
        next_element.shell.present?)
  end
rescue StandardError
  wwe $!
end

#generate_env_variable_shell_commands(fcb) ⇒ Array<String>

Generates shell code lines to set environment variables named in the body of the given object. Reads a whitespace-separated list of environment variable names from ‘fcb.body`, retrieves their values from the current environment, and constructs shell commands to set these environment variables.

Example:

If `fcb.body` returns ["PATH", "HOME"], and the current environment has PATH=/usr/bin
and HOME=/home/user, this method will return:
  ["PATH=/usr/bin", "HOME=/home/user"]

Parameters:

  • An object with a ‘body` method that returns an array of strings, where each string is a name of an environment variable.

Returns:

  • An array of strings, each representing a shell command to set an environment variable in the format ‘KEY=value`.



339
340
341
342
343
# File 'lib/mdoc.rb', line 339

def generate_env_variable_shell_commands(fcb)
  fcb.body.join(' ').split.compact.map do |key|
    "#{key}=#{Shellwords.escape ENV.fetch(key, '')}"
  end
end

#generate_yq_command_from_call_annotation(fcb) ⇒ String

Generates a yq (YAML query) shell command from a call annotation. The call annotation specifies I/O redirection patterns:

  • <$VAR_NAME: read YAML from environment variable, apply expression from block body

  • <FILE_NAME: read YAML from file, apply expression from block body

  • >$VAR_NAME: write result to environment variable

  • >FILE_NAME: write result to file

Parameters:

  • The fenced code block containing the call annotation and body

Returns:

  • A shell command string that executes the yq query



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/mdoc.rb', line 80

def generate_yq_command_from_call_annotation(fcb)
  body = fcb.body.join("\n")
  xcall = fcb[:cann][1..-2]
  # match <$VAR_NAME, <FILE_NAME, >$VAR_NAME, >FILE_NAME
  mstdin = xcall.match(/<(?<type>\$)?(?<name>[\-.\w]+)/)
  mstdout = xcall.match(/>(?<type>\$)?(?<name>[\-.\w]+)/)

  yqcmd = if mstdin[:type]
            # when <$VAR_NAME
            # the value of the variable is the expression to apply to the YAML from the block body.
            "echo \"$#{mstdin[:name]}\" | yq '#{body}'"
          else
            # when <FILE_NAME
            # the block body is the expression to apply to the YAML file.
            "yq e '#{body}' '#{mstdin[:name]}'"
          end
  if mstdout[:type]
    # when >$VAR_NAME
    # set the value of the variable to the result of the expression
    "export #{mstdout[:name]}=$(#{yqcmd})"
  else
    # when >FILE_NAME
    # write the result of the expression to the file
    "#{yqcmd} > '#{mstdout[:name]}'"
  end
end

#get_block_by_anyname(name, default = {}) ⇒ Hash

Retrieves a code block by its name.

Parameters:

  • The name of the code block to retrieve.

  • (defaults to: {})

    The default value to return if the code block is not found.

Returns:

  • The code block as a hash or the default value if not found.



390
391
392
393
394
# File 'lib/mdoc.rb', line 390

def get_block_by_anyname(name, default = {})
  table_not_split.select do |fcb|
    fcb.is_named?(name)
  end.fetch(0, default)
end

#get_blocks_by_anyname(name) ⇒ Hash

Retrieves code blocks by a name.

Parameters:

  • The name of the code block to retrieve.

  • The default value to return if the code block is not found.

Returns:

  • The code block as a hash or the default value if not found.



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

def get_blocks_by_anyname(name)
  table_not_split.select do |fcb|
    fcb.is_named?(name)
  end
end

#hide_menu_block_on_name(opts, block) ⇒ Boolean

Checks if a code block should be hidden based on the given options.

:reek:UtilityFunction

Parameters:

  • The options used for hiding code blocks.

  • The code block to check for hiding.

Returns:

  • True if the code block should be hidden; false otherwise.



415
416
417
418
419
420
421
422
423
424
425
426
427
428
# File 'lib/mdoc.rb', line 415

def hide_menu_block_on_name(opts, block)
  if block.fetch(:chrome, false)
    false
  else
    opts[:hide_blocks_by_name] &&
      ((opts[:block_name_hide_custom_match]&.present? &&
        block.s2title&.match(Regexp.new(opts[:block_name_hide_custom_match]))) ||
       (opts[:block_name_hidden_match]&.present? &&
        block.s2title&.match(Regexp.new(opts[:block_name_hidden_match]))) ||
       (opts[:block_name_wrapper_match]&.present? &&
        block.s2title&.match(Regexp.new(opts[:block_name_wrapper_match])))) &&
      (block.s2title&.present? || block[:label]&.present?)
  end
end

#process_block_to_code(fcb, block_source, label_body, label_format_above, label_format_below, context_code: []) ⇒ String, ...

Processes a single code block and returns its code representation.

Parameters:

  • The code block to process.

  • Additional information for label generation.

  • Whether to generate labels around the body.

  • Format string for label above content.

  • Format string for label below content.

Returns:

  • The code representation of the block, or nil if the block should be skipped.



439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
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
500
501
# File 'lib/mdoc.rb', line 439

def process_block_to_code(fcb, block_source, label_body, label_format_above,
                          label_format_below, context_code: [])
  raise 'unexpected label_body' unless label_body

  new_context_code = []

  new_transient_code = if fcb[:cann]
                         generate_yq_command_from_call_annotation(fcb)
                       elsif fcb[:stdout]
                         # copy the block's body to either a shell variable or a file
                         code_for_fcb_body_into_var_or_file(fcb)
                       elsif [BlockType::OPTS].include? fcb.type
                         fcb.body # entire body is returned to requesing block

                       elsif [BlockType::LINK,
                              BlockType::LOAD,
                              BlockType::UX,
                              BlockType::VARS].include? fcb.type
                         nil # Vars for all types are collected later
                       elsif fcb[:chrome] # for Link blocks like History
                         nil
                       elsif fcb.type == BlockType::PORT
                         generate_env_variable_shell_commands(fcb)
                       elsif label_body
                         raise 'unexpected type' if fcb.type != BlockType::SHELL

                         # BlockType::  SHELL block
                         if fcb.start_line =~ /@eval/
                           command_result = HashDelegator.execute_bash_script_lines(
                             transient_code: context_code + fcb.body,
                             export: OpenStruct.new(exportable: false, name: ''),
                             export_name: '',
                             force: true,
                             shell: fcb.shell || 'bash'
                           )
                           command_result.stdout.split("\n").tap do
                             if fcb.start_line =~ /@context/
                               new_context_code = _1
                             end
                           end
                         elsif fcb.start_line =~ /@context/
                           # raw body
                           ### expansions?
                           new_context_code = fcb.body
                           [] # collect later or return as code to inherit

                         else
                           wrap_block_body_with_labels(
                             fcb, block_source,
                             label_format_above, label_format_below
                           )
                         end
                       else # raw body
                         fcb.body
                       end

  OpenStruct.new(
    context_code: new_context_code,
    transient_code: new_transient_code
  ).tap { wwr _1 }
rescue StandardError
  wwe $!
end

#recursively_required(reqs) ⇒ Array<String>

Recursively fetches required code blocks for a given list of requirements.

Parameters:

  • An array of requirements to start the recursion from.

Returns:

  • An array of recursively required code block names.



508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
# File 'lib/mdoc.rb', line 508

def recursively_required(reqs)
  return [] unless reqs

  rem = reqs
  memo = []
  while rem && rem.count.positive?
    rem = rem.map do |req|
      next if memo.include? req

      memo += [req]
      get_block_by_anyname(req).reqs
    end
             .compact
             .flatten(1)
  end
  memo
end

#recursively_required_hash(source, memo = Hash.new([])) ⇒ Hash

Recursively fetches required code blocks for a given list of requirements.

Parameters:

  • The name of the code block to start the recursion from.

Returns:

  • A list of code blocks required by each source code block.



531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
# File 'lib/mdoc.rb', line 531

def recursively_required_hash(source, memo = Hash.new([]))
  return memo unless source
  return memo if memo.keys.include? source

  blocks = get_blocks_by_anyname(source)
  if blocks.empty?
    raise "Named code block `#{source}` not found. (@#{__LINE__})"
  end

  memo[source] = blocks.map(&:reqs).flatten(1)
  return memo unless memo[source]&.count&.positive?

  memo[source].each do |req|
    next if memo.keys.include? req

    recursively_required_hash(req, memo)
  end
  memo
end

#select_elements_with_neighbor_conditions(array, last_selected_placeholder = nil, next_selected_placeholder = nil) ⇒ Object



585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
# File 'lib/mdoc.rb', line 585

def select_elements_with_neighbor_conditions(
  array,
  last_selected_placeholder = nil,
  next_selected_placeholder = nil
)
  selected_elements = []
  last_selected = last_selected_placeholder

  array.each_with_index do |current, index|
    next_element = if index < array.size - 1
                     array[index + 1]
                   else
                     next_selected_placeholder
                   end

    if yield(last_selected, current, next_element)
      selected_elements << current
      last_selected = current
    end
  end

  selected_elements
end

#table_not_splitObject

exclude blocks with duplicate code the first block in each split contains the same data as the rest of the split



611
612
613
# File 'lib/mdoc.rb', line 611

def table_not_split
  @table.reject(&:is_split_rest?)
end

#wrap_block_body_with_labels(fcb, block_source, label_format_above, label_format_below) ⇒ Array<String>

Wraps a code block body with formatted labels above and below the main content. The labels and content are based on the provided format strings and the body of the given object.

Example:

If `fcb.pub_name` returns "Example Block", `fcb.body` returns ["line1", "line2"],
`block_source` is { source: "source_info" }, `label_format_above` is "Start of %{block_name}",
and `label_format_below` is "End of %{block_name}", the method will return:
  ["Start of Example_Block", "line1", "line2", "End of Example_Block"]

Parameters:

  • An object with a ‘pub_name` method that returns a string, and a `body` method that returns an array of strings.

  • A hash containing additional information to be merged into the format strings.

  • A format string for the label above the content, or nil if no label is needed.

  • A format string for the label below the content, or nil if no label is needed.

Returns:

  • An array of strings representing the formatted code block, with optional labels above and below the main content.



360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/mdoc.rb', line 360

def wrap_block_body_with_labels(fcb, block_source, label_format_above,
                                label_format_below)
  block_name_for_bash_comment = fcb.pub_name.gsub(/\s+/, '_')

  label_above = if label_format_above.present?
                  format(label_format_above,
                         block_source.merge(
                           { block_name: block_name_for_bash_comment }
                         ))
                else
                  nil
                end
  label_below = if label_format_below.present?
                  format(label_format_below,
                         block_source.merge(
                           { block_name: block_name_for_bash_comment }
                         ))
                else
                  nil
                end

  [label_above, *fcb.body, label_below].compact
end