Class: MarkdownExec::MDoc
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
-
#table ⇒ Object
readonly
Returns the value of attribute table.
Instance Method Summary collapse
-
#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.
-
#collect_block_dependencies(anyname:) ⇒ Array<Hash>
Retrieves code blocks that are required by a specified code block.
-
#collect_dependencies(block: nil, memo: {}, pubname: nil) ⇒ Hash
Recursively collects dependencies of a given source.
-
#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.
- #collect_unique_names(hash) ⇒ Object
-
#collect_wrapped_blocks(blocks) ⇒ Array<Hash>
Retrieves code blocks that are wrapped wraps are applied from left to right e.g.
- #error_handler(name = '', opts = {}) ⇒ Object
-
#fcbs_per_options(opts = {}) ⇒ Array<Hash>
Retrieves code blocks based on the provided options.
-
#generate_env_variable_shell_commands(fcb) ⇒ Array<String>
Generates shell code lines to set environment variables named in the body of the given object.
-
#generate_yq_command_from_call_annotation(fcb) ⇒ String
Generates a yq (YAML query) shell command from a call annotation.
-
#get_block_by_anyname(name, default = {}) ⇒ Hash
Retrieves a code block by its name.
-
#get_blocks_by_anyname(name) ⇒ Hash
Retrieves code blocks by a name.
-
#hide_menu_block_on_name(opts, block) ⇒ Boolean
Checks if a code block should be hidden based on the given options.
-
#initialize(table = []) ⇒ MDoc
constructor
Initializes an instance of MDoc with the given table of markdown sections.
-
#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.
-
#recursively_required(reqs) ⇒ Array<String>
Recursively fetches required code blocks for a given list of requirements.
-
#recursively_required_hash(source, memo = Hash.new([])) ⇒ Hash
Recursively fetches required code blocks for a given list of requirements.
- #select_elements_with_neighbor_conditions(array, last_selected_placeholder = nil, next_selected_placeholder = nil) ⇒ Object
-
#table_not_split ⇒ Object
exclude blocks with duplicate code the first block in each split contains the same data as the rest of the split.
-
#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.
Constructor Details
#initialize(table = []) ⇒ MDoc
Initializes an instance of MDoc with the given table of markdown sections.
67 68 69 |
# File 'lib/mdoc.rb', line 67 def initialize(table = []) @table = table end |
Instance Attribute Details
#table ⇒ Object (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.
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.
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.
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.
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
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.
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 (opts = {}) = opts.merge(block_name_hide_custom_match: nil) selrows = @table.select do |fcb_title_groups| Filter.fcb_select? , 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| (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. # 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"]
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
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.
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.
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
415 416 417 418 419 420 421 422 423 424 425 426 427 428 |
# File 'lib/mdoc.rb', line 415 def (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.
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.
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.
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_split ⇒ Object
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"]
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 |