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:

  • table (Array<Hash>) (defaults to: [])

    An array of hashes representing markdown sections.



26
27
28
29
# File 'lib/mdoc.rb', line 26

def initialize(table = [])
  @table = table
  # !!t @table.count
end

Instance Attribute Details

#tableObject (readonly)

Returns the value of attribute table.



20
21
22
# File 'lib/mdoc.rb', line 20

def table
  @table
end

Instance Method Details

#collect_block_code_cann(fcb) ⇒ Object



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/mdoc.rb', line 31

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.

Parameters:

  • fcb (Hash)

    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:

  • (String)

    A string containing the formatted shell command to output 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.



60
61
62
63
64
65
66
67
68
69
70
# File 'lib/mdoc.rb', line 60

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.

Parameters:

  • name (String)

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

Returns:

  • (Array<Hash>)

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



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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/mdoc.rb', line 77

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

  dependencies = collect_dependencies(nickname)
  # !!t dependencies.count
  all_dependency_names = collect_unique_names(dependencies).push(nickname).uniq
  # !!t all_dependency_names.count

  # select blocks in order of appearance in source documents
  #
  blocks = @table.select do |fcb|
    # 2024-08-04 match nickname
    all_dependency_names.include?(fcb.pub_name) || all_dependency_names.include?(fcb.nickname) || all_dependency_names.include?(fcb.oname)
  end
  # !!t blocks.count

  ## add cann key to blocks, calc unmet_dependencies
  #
  unmet_dependencies = all_dependency_names.dup
  blocks = blocks.map do |fcb|
    # 2024-08-04 match oname for long block names
    # 2024-08-04 match nickname
    unmet_dependencies.delete(fcb.pub_name) || unmet_dependencies.delete(fcb.nickname) || unmet_dependencies.delete(fcb.oname) # may not exist if block name is duplicated
    if (call = fcb.call)
      fcb1 = get_block_by_anyname("[#{call.match(/^%\((\S+) |\)/)[1]}]")
      fcb1.cann = call
      [fcb1]
    else
      []
    end + [fcb]
  end.flatten(1)
  # !!t unmet_dependencies.count

  { all_dependency_names: all_dependency_names,
    blocks: blocks,
    dependencies: dependencies,
    unmet_dependencies: unmet_dependencies }
end

#collect_dependencies(source, memo = {}) ⇒ Hash

Recursively collects dependencies of a given source.

Parameters:

  • source (String)

    The name of the initial source block.

  • memo (Hash) (defaults to: {})

    A memoization hash to store resolved dependencies.

Returns:

  • (Hash)

    A hash mapping sources to their respective dependencies.



381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/mdoc.rb', line 381

def collect_dependencies(source, memo = {})
  return memo unless source

  block = get_block_by_anyname(source)
  if block.nil? || block.instance_of?(Hash)
    raise "Named code block `#{source}` not found. (@#{__LINE__})"
  end

  return memo unless block.reqs

  memo[source] = block.reqs

  block.reqs.each do |req|
    collect_dependencies(req, memo) unless memo.key?(req)
  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.

Parameters:

  • name (String)

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

Returns:

  • (Array<String>)

    An array of strings containing the collected code blocks.



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

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::VARS].include? fcb.type
            nil
          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



165
166
167
# File 'lib/mdoc.rb', line 165

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:

  • (Array<Hash>)

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



175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/mdoc.rb', line 175

def collect_wrapped_blocks(blocks)
  blocks.map do |block|
    (block[:wraps] || []).map do |wrap|
      wrap_before = wrap.sub('}', '-before}') ### hardcoded wrap name
      @table.select { |fcb| [wrap_before, wrap].include? fcb.oname }
    end.flatten(1) +
      [block] +
      (block[:wraps] || []).reverse.map do |wrap|
        wrap_after = wrap.sub('}', '-after}') ### hardcoded wrap name
        @table.select { |fcb| fcb.oname == wrap_after }
      end.flatten(1)
  end.flatten(1).compact
end

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



189
190
191
192
193
194
# File 'lib/mdoc.rb', line 189

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:

  • opts (Hash) (defaults to: {})

    The options used for filtering code blocks.

Returns:

  • (Array<Hash>)

    An array of code blocks that match the options.



201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/mdoc.rb', line 201

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 options[:menu_include_imported_blocks]
    selrows = selrows.reject do |block|
      block.fetch(:depth, 0).positive?
    end
  end

  if opts[:hide_blocks_by_name]
    selrows = selrows.reject do |block|
      hide_menu_block_on_name opts, block
    end
  end

  # 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"]

Parameters:

  • fcb (Object)

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

Returns:

  • (Array<String>)

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



248
249
250
251
252
# File 'lib/mdoc.rb', line 248

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"]

Parameters:

  • fcb (Object)

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

  • block_source (Hash)

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

  • label_format_above (String, nil)

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

  • label_format_below (String, nil)

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

Returns:

  • (Array<String>)

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



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/mdoc.rb', line 269

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.

Parameters:

  • name (String)

    The name of the code block to retrieve.

  • default (Hash) (defaults to: {})

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

Returns:

  • (Hash)

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



295
296
297
298
299
300
301
302
303
# File 'lib/mdoc.rb', line 295

def get_block_by_anyname(name, default = {})
  # !!t name
  @table.select do |fcb|
    fcb.nickname == name ||
      fcb.dname == name ||
      fcb.oname == name ||
      fcb.pub_name == name
  end.fetch(0, default)
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:

  • opts (Hash)

    The options used for hiding code blocks.

  • block (Hash)

    The code block to check for hiding.

Returns:

  • (Boolean)

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



312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/mdoc.rb', line 312

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.oname&.match(Regexp.new(opts[:block_name_hidden_match]))) ||
       (opts[:block_name_include_match]&.present? &&
        block.oname&.match(Regexp.new(opts[:block_name_include_match]))) ||
       (opts[:block_name_wrapper_match]&.present? &&
        block.oname&.match(Regexp.new(opts[:block_name_wrapper_match])))) &&
      (block.oname&.present? || block[:label]&.present?)

  end
end

#recursively_required(reqs) ⇒ Array<String>

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

Parameters:

  • reqs (Array<String>)

    An array of requirements to start the recursion from.

Returns:

  • (Array<String>)

    An array of recursively required code block names.



333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/mdoc.rb', line 333

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:

  • source (String)

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

Returns:

  • (Hash)

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



356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
# File 'lib/mdoc.rb', line 356

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

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

  memo[source] = block.reqs
  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



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

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