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.



24
25
26
27
# File 'lib/mdoc.rb', line 24

def initialize(table = [])
  @table = table
  # &bc '@table.count:',@table.count
end

Instance Attribute Details

#tableObject (readonly)

Returns the value of attribute table.



18
19
20
# File 'lib/mdoc.rb', line 18

def table
  @table
end

Instance Method Details

#collect_block_code_cann(fcb) ⇒ Object



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

def collect_block_code_cann(fcb)
  body = fcb[:body].join("\n")
  xcall = fcb[:cann][1..-2]
  mstdin = xcall.match(/<(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/)
  mstdout = xcall.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\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_shell(fcb) ⇒ Object



47
48
49
50
51
52
53
54
# File 'lib/mdoc.rb', line 47

def collect_block_code_shell(fcb)
  # write named variables to block at top of script
  #
  fcb[:body].join(' ').split.compact.map do |key|
    ### format(opts[:block_type_port_set_format], { key: key, value: ENV.fetch(key, nil) })
    "key: #{key}, value: #{ENV.fetch(key, nil)}"
  end
end

#collect_block_code_stdout(fcb) ⇒ Object



56
57
58
59
60
61
62
63
64
65
66
# File 'lib/mdoc.rb', line 56

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.



73
74
75
76
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
# File 'lib/mdoc.rb', line 73

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[:nickname] || name_block[:oname]
  dependencies = collect_dependencies(nickname)
  # &bc 'dependencies.count:',dependencies.count
  all_dependency_names = collect_unique_names(dependencies).push(nickname).uniq
  # &bc 'all_dependency_names.count:',all_dependency_names.count

  # select non-chrome blocks in order of appearance in source documents
  #
  blocks = @table.select do |fcb|
    !fcb.fetch(:chrome,
               false) && all_dependency_names.include?(fcb.fetch(:nickname,
                                                                 nil) || fcb.fetch(:oname))
  end
  # &bc 'blocks.count:',blocks.count

  ## add cann key to blocks, calc unmet_dependencies
  #
  unmet_dependencies = all_dependency_names.dup
  blocks = blocks.map do |fcb|
    unmet_dependencies.delete(fcb[:nickname] || fcb[:oname]) # may not exist if block name is duplicated
    if (call = fcb[:call])
      [get_block_by_anyname("[#{call.match(/^%\((\S+) |\)/)[1]}]")
        .merge({ cann: call })]
    else
      []
    end + [fcb]
  end.flatten(1)
  # &bc 'unmet_dependencies.count:',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.



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

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

  if (block = get_block_by_anyname(source)).nil? || block.keys.empty?
    return memo if true

    raise "Named code block `#{source}` not found. (@#{__LINE__})"

  end

  return memo unless block[:reqs]

  memo[source] = block[:reqs]

  block[:reqs].each { |req| collect_dependencies(req, memo) unless memo.key?(req) }
  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.



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

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])
    # &bc 'blocks.count:',blocks.count

    block_search.merge(
      { block_names: blocks.map { |block| block[:nickname] || block[:oname] },
        code: blocks.map do |fcb|
          if fcb[:cann]
            collect_block_code_cann(fcb)
          elsif fcb[:stdout]
            collect_block_code_stdout(fcb)
          elsif [BlockType::LINK, BlockType::OPTS,
                 BlockType::VARS].include? fcb[:shell]
            nil
          elsif fcb[:shell] == BlockType::PORT
            collect_block_code_shell(fcb)
          elsif label_body
            block_name_for_bash_comment = (fcb[:nickname] || fcb[:oname]).gsub(/\s+/, '_')
            [label_format_above && format(label_format_above,
                                          block_source.merge({ block_name: block_name_for_bash_comment }))] +
             fcb[:body] +
             [label_format_below && format(label_format_below,
                                           block_source.merge({ block_name: block_name_for_bash_comment }))]
          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



157
158
159
# File 'lib/mdoc.rb', line 157

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.



167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/mdoc.rb', line 167

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



181
182
183
184
185
186
# File 'lib/mdoc.rb', line 181

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.



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

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

  if !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

#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.



227
228
229
230
231
232
# File 'lib/mdoc.rb', line 227

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



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

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.



262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/mdoc.rb', line 262

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).fetch(: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.



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'lib/mdoc.rb', line 285

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.empty?
    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



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

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