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.



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

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

Instance Attribute Details

#tableObject (readonly)

Returns the value of attribute table.



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

def table
  @table
end

Instance Method Details

#collect_block_code_cann(fcb) ⇒ Object



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/mdoc.rb', line 70

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

  yqcmd = if mstdin[:type]
            "echo \"$#{mstdin[:name]}\" | yq '#{body}'"
          else
            "yq e '#{body}' '#{mstdin[:name]}'"
          end
  if mstdout[:type]
    "export #{mstdout[:name]}=$(#{yqcmd})"
  else
    "#{yqcmd} > '#{mstdout[:name]}'"
  end
end

#collect_block_code_stdout(fcb) ⇒ String

Collects and formats the shell command output to redirect script block code to a file or a variable.



99
100
101
102
103
104
105
106
107
108
109
# File 'lib/mdoc.rb', line 99

def collect_block_code_stdout(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.



116
117
118
119
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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/mdoc.rb', line 116

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.



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
# File 'lib/mdoc.rb', line 468

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) ⇒ Array<String>

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



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
212
# File 'lib/mdoc.rb', line 169

def collect_recursively_required_code(
  anyname:, block_source:,
  label_body: true, label_format_above: nil, label_format_below: nil
)
  block_search = collect_block_dependencies(anyname: anyname)
  if block_search[:blocks]
    blocks = collect_wrapped_blocks(block_search[:blocks])
    # !!t blocks.count

    block_search.merge(
      { block_names: blocks.map(&:pub_name),
        code: blocks.map do |fcb|
          if fcb[:cann]
            collect_block_code_cann(fcb)
          elsif fcb[:stdout]
            collect_block_code_stdout(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
            generate_label_body_code(
              fcb, block_source,
              label_format_above, label_format_below
            )
          else # raw body
            fcb.body
          end
        end.compact.flatten(1).compact }
    )
  else
    block_search.merge({ block_names: [], code: [] })
  end
rescue StandardError
  error_handler('collect_recursively_required_code')
end

#collect_unique_names(hash) ⇒ Object



214
215
216
# File 'lib/mdoc.rb', line 214

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



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/mdoc.rb', line 224

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
end

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



242
243
244
245
246
247
# File 'lib/mdoc.rb', line 242

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.



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
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
# File 'lib/mdoc.rb', line 254

def fcbs_per_options(opts = {})
  options = opts.merge(block_name_hidden_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
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"]


325
326
327
328
329
# File 'lib/mdoc.rb', line 325

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

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

Generates a formatted code block with 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"]


346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
# File 'lib/mdoc.rb', line 346

def generate_label_body_code(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

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

Retrieves a code block by its name.



376
377
378
379
380
# File 'lib/mdoc.rb', line 376

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.



388
389
390
391
392
# File 'lib/mdoc.rb', line 388

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



401
402
403
404
405
406
407
408
409
410
411
412
413
414
# File 'lib/mdoc.rb', line 401

def hide_menu_block_on_name(opts, block)
  if block.fetch(:chrome, false)
    false
  else
    opts[:hide_blocks_by_name] &&
      ((opts[:block_name_hidden_match]&.present? &&
        block.s2title&.match(Regexp.new(opts[:block_name_hidden_match]))) ||
       (opts[:block_name_include_match]&.present? &&
        block.s2title&.match(Regexp.new(opts[:block_name_include_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

#recursively_required(reqs) ⇒ Array<String>

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



421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
# File 'lib/mdoc.rb', line 421

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.



444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
# File 'lib/mdoc.rb', line 444

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



498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
# File 'lib/mdoc.rb', line 498

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



524
525
526
# File 'lib/mdoc.rb', line 524

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