markdown_exec 1.3.9 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/markdown_exec.rb CHANGED
@@ -9,6 +9,7 @@ require 'fileutils'
9
9
  require 'open3'
10
10
  require 'optparse'
11
11
  require 'shellwords'
12
+ require 'tmpdir'
12
13
  require 'tty-prompt'
13
14
  require 'yaml'
14
15
 
@@ -19,13 +20,13 @@ require_relative 'colorize'
19
20
  require_relative 'env'
20
21
  require_relative 'fcb'
21
22
  require_relative 'filter'
23
+ require_relative 'markdown_exec/version'
22
24
  require_relative 'mdoc'
23
25
  require_relative 'option_value'
24
26
  require_relative 'saved_assets'
25
27
  require_relative 'saved_files_matcher'
26
28
  require_relative 'shared'
27
29
  require_relative 'tap'
28
- require_relative 'markdown_exec/version'
29
30
 
30
31
  include CLI
31
32
  include Tap
@@ -35,14 +36,11 @@ tap_config envvar: MarkdownExec::TAP_DEBUG
35
36
  $stderr.sync = true
36
37
  $stdout.sync = true
37
38
 
38
- BLOCK_SIZE = 1024
39
+ MDE_HISTORY_ENV_NAME = 'MDE_MENU_HISTORY'
39
40
 
40
41
  # macros
41
42
  #
42
- BACK_OPTION = '* Back'
43
- EXIT_OPTION = '* Exit'
44
43
  LOAD_FILE = true
45
- VN = 'MDE_MENU_HISTORY'
46
44
 
47
45
  # custom error: file specified is missing
48
46
  #
@@ -129,10 +127,45 @@ end
129
127
  # convert regex match groups to a hash with symbol keys
130
128
  #
131
129
  # :reek:UtilityFunction
132
- def option_match_groups(str, option)
130
+ def extract_named_captures_from_option(str, option)
133
131
  str.match(Regexp.new(option))&.named_captures&.sym_keys
134
132
  end
135
133
 
134
+ module ArrayUtil
135
+ def self.partition_by_predicate(arr)
136
+ true_list = []
137
+ false_list = []
138
+
139
+ arr.each do |element|
140
+ if yield(element)
141
+ true_list << element
142
+ else
143
+ false_list << element
144
+ end
145
+ end
146
+
147
+ [true_list, false_list]
148
+ end
149
+ end
150
+
151
+ module StringUtil
152
+ # Splits the given string on the first occurrence of the specified character.
153
+ # Returns an array containing the portion of the string before the character and the rest of the string.
154
+ #
155
+ # @param input_str [String] The string to be split.
156
+ # @param split_char [String] The character on which to split the string.
157
+ # @return [Array<String>] An array containing two elements: the part of the string before split_char, and the rest of the string.
158
+ def self.partition_at_first(input_str, split_char)
159
+ split_index = input_str.index(split_char)
160
+
161
+ if split_index.nil?
162
+ [input_str, '']
163
+ else
164
+ [input_str[0...split_index], input_str[(split_index + 1)..-1]]
165
+ end
166
+ end
167
+ end
168
+
136
169
  # execute markdown documents
137
170
  #
138
171
  module MarkdownExec
@@ -141,10 +174,10 @@ module MarkdownExec
141
174
  FNR12 = ',~'
142
175
 
143
176
  SHELL_COLOR_OPTIONS = {
144
- 'bash' => :menu_bash_color,
177
+ BLOCK_TYPE_BASH => :menu_bash_color,
145
178
  BLOCK_TYPE_LINK => :menu_link_color,
146
- 'opts' => :menu_opts_color,
147
- 'vars' => :menu_vars_color
179
+ BLOCK_TYPE_OPTS => :menu_opts_color,
180
+ BLOCK_TYPE_VARS => :menu_vars_color
148
181
  }.freeze
149
182
 
150
183
  ##
@@ -158,12 +191,11 @@ module MarkdownExec
158
191
  class MarkParse
159
192
  attr_reader :options
160
193
 
194
+ include ArrayUtil
195
+ include StringUtil
161
196
  include FOUT
162
197
 
163
198
  def initialize(options = {})
164
- @options = options
165
- # hide disabled symbol
166
- @prompt = TTY::Prompt.new(interrupt: :exit, symbols: { cross: ' ' })
167
199
  @execute_aborted_at = nil
168
200
  @execute_completed_at = nil
169
201
  @execute_error = nil
@@ -173,6 +205,74 @@ module MarkdownExec
173
205
  @execute_script_filespec = nil
174
206
  @execute_started_at = nil
175
207
  @option_parser = nil
208
+ @options = options
209
+ @prompt = tty_prompt_without_disabled_symbol
210
+ end
211
+
212
+ ##
213
+ # Appends a summary of a block (FCB) to the blocks array.
214
+ #
215
+ def append_block_summary(blocks, fcb, opts)
216
+ ## enhance fcb with block summary
217
+ #
218
+ blocks.push get_block_summary(opts, fcb)
219
+ end
220
+
221
+ ##
222
+ # Appends a final divider to the blocks array if it is specified in options.
223
+ #
224
+ def append_final_divider(blocks, opts)
225
+ return unless opts[:menu_divider_format].present? && opts[:menu_final_divider].present?
226
+
227
+ blocks.push FCB.new(
228
+ { chrome: true,
229
+ disabled: '',
230
+ dname: format(opts[:menu_divider_format],
231
+ opts[:menu_final_divider])
232
+ .send(opts[:menu_divider_color].to_sym),
233
+ oname: opts[:menu_final_divider] }
234
+ )
235
+ end
236
+
237
+ ##
238
+ # Appends an initial divider to the blocks array if it is specified in options.
239
+ #
240
+ def append_initial_divider(blocks, opts)
241
+ return unless opts[:menu_initial_divider].present?
242
+
243
+ blocks.push FCB.new({
244
+ # name: '',
245
+ chrome: true,
246
+ dname: format(
247
+ opts[:menu_divider_format],
248
+ opts[:menu_initial_divider]
249
+ ).send(opts[:menu_divider_color].to_sym),
250
+ oname: opts[:menu_initial_divider],
251
+ disabled: '' # __LINE__.to_s
252
+ })
253
+ end
254
+
255
+ # Execute a code block after approval and provide user interaction options.
256
+ #
257
+ # This method displays required code blocks, asks for user approval, and
258
+ # executes the code block if approved. It also allows users to copy the
259
+ # code to the clipboard or save it to a file.
260
+ #
261
+ # @param opts [Hash] Options hash containing configuration settings.
262
+ # @param mdoc [YourMDocClass] An instance of the MDoc class.
263
+ #
264
+ def approve_and_execute_block(opts, mdoc)
265
+ selected = mdoc.get_block_by_name(opts[:block_name])
266
+
267
+ if selected.fetch(:shell, '') == BLOCK_TYPE_LINK
268
+ handle_shell_link(opts, selected.fetch(:body, ''), mdoc)
269
+ elsif opts.fetch(:back, false)
270
+ handle_back_link(opts)
271
+ elsif selected[:shell] == BLOCK_TYPE_OPTS
272
+ handle_shell_opts(opts, selected)
273
+ else
274
+ handle_remainder_blocks(mdoc, opts, selected)
275
+ end
176
276
  end
177
277
 
178
278
  # return arguments before `--`
@@ -206,6 +306,14 @@ module MarkdownExec
206
306
  end.compact.to_h
207
307
  end
208
308
 
309
+ def blocks_per_opts(blocks, opts)
310
+ return blocks if opts[:struct]
311
+
312
+ blocks.map do |block|
313
+ block.fetch(:text, nil) || block.oname
314
+ end.compact.reject(&:empty?)
315
+ end
316
+
209
317
  def calculated_options
210
318
  {
211
319
  bash: true, # bash block parsing in get_block_summary()
@@ -214,100 +322,22 @@ module MarkdownExec
214
322
  }
215
323
  end
216
324
 
217
- # Execute a code block after approval and provide user interaction options.
218
- #
219
- # This method displays required code blocks, asks for user approval, and
220
- # executes the code block if approved. It also allows users to copy the
221
- # code to the clipboard or save it to a file.
222
- #
223
- # @param opts [Hash] Options hash containing configuration settings.
224
- # @param mdoc [YourMDocClass] An instance of the MDoc class.
225
- # @return [String] The name of the executed code block.
226
- #
227
- def approve_and_execute_block(opts, mdoc)
228
- selected = mdoc.get_block_by_name(opts[:block_name])
229
- if selected[:shell] == BLOCK_TYPE_LINK
230
- handle_link_shell(opts, selected)
231
- elsif selected[:shell] == 'opts'
232
- handle_opts_shell(opts, selected)
233
- else
234
- required_lines = collect_required_code_blocks(opts, mdoc, selected)
235
- # Display required code blocks if requested or required approval.
236
- if opts[:output_script] || opts[:user_must_approve]
237
- display_required_code(opts, required_lines)
238
- end
239
-
240
- allow = true
241
- allow = user_approval(opts, required_lines) if opts[:user_must_approve]
242
- opts[:ir_approve] = allow
243
- mdoc.get_block_by_name(opts[:block_name])
244
- execute_approved_block(opts, required_lines) if opts[:ir_approve]
245
-
246
- [!LOAD_FILE, '']
247
- end
248
- end
249
-
250
- def handle_link_shell(opts, selected)
251
- data = YAML.load(selected[:body].join("\n"))
252
-
253
- # add to front of history
254
- #
255
- ENV[VN] = opts[:filename] + opts[:history_document_separator] + ENV.fetch(VN, '')
256
-
257
- opts[:filename] = data.fetch('file', nil)
258
- return !LOAD_FILE unless opts[:filename]
259
-
260
- data.fetch('vars', []).each do |var|
261
- ENV[var[0]] = var[1].to_s
262
- end
263
-
264
- [LOAD_FILE, data.fetch('block', '')]
265
- end
266
-
267
- def handle_opts_shell(opts, selected)
268
- data = YAML.load(selected[:body].join("\n"))
269
- data.each_key do |key|
270
- opts[key.to_sym] = value = data[key].to_s
271
- next unless opts[:menu_opts_set_format].present?
272
-
273
- print format(
274
- opts[:menu_opts_set_format],
275
- { key: key,
276
- value: value }
277
- ).send(opts[:menu_opts_set_color].to_sym)
278
- end
279
- [!LOAD_FILE, '']
280
- end
281
-
282
- def user_approval(opts, required_lines)
283
- # Present a selection menu for user approval.
284
- sel = @prompt.select(opts[:prompt_approve_block], filter: true) do |menu|
285
- menu.default 1
286
- menu.choice opts[:prompt_yes], 1
287
- menu.choice opts[:prompt_no], 2
288
- menu.choice opts[:prompt_script_to_clipboard], 3
289
- menu.choice opts[:prompt_save_script], 4
325
+ # Check whether the document exists and is readable
326
+ def check_file_existence(filename)
327
+ unless filename&.present?
328
+ fout 'No blocks found.'
329
+ return false
290
330
  end
291
331
 
292
- if sel == 3
293
- copy_to_clipboard(required_lines)
294
- elsif sel == 4
295
- save_to_file(opts, required_lines)
332
+ unless File.exist? filename
333
+ fout 'Document is missing.'
334
+ return false
296
335
  end
297
-
298
- sel == 1
336
+ true
299
337
  end
300
338
 
301
- def execute_approved_block(opts, required_lines)
302
- write_command_file(opts, required_lines)
303
- command_execute(
304
- opts,
305
- required_lines.flatten.join("\n"),
306
- args: opts.fetch(:pass_args, [])
307
- )
308
- save_execution_output
309
- output_execution_summary
310
- output_execution_result
339
+ def clear_required_file
340
+ ENV['MDE_LINK_REQUIRED_FILE'] = ''
311
341
  end
312
342
 
313
343
  # Collect required code blocks based on the provided options.
@@ -315,11 +345,7 @@ module MarkdownExec
315
345
  # @param opts [Hash] Options hash containing configuration settings.
316
346
  # @param mdoc [YourMDocClass] An instance of the MDoc class.
317
347
  # @return [Array<String>] Required code blocks as an array of lines.
318
- def collect_required_code_blocks(opts, mdoc, selected)
319
- required = mdoc.collect_recursively_required_code(opts[:block_name])
320
- required_lines = required[:code]
321
- required[:blocks]
322
-
348
+ def collect_required_code_lines(mdoc, selected, opts: {})
323
349
  # Apply hash in opts block to environment variables
324
350
  if selected[:shell] == BLOCK_TYPE_VARS
325
351
  data = YAML.load(selected[:body].join("\n"))
@@ -335,33 +361,20 @@ module MarkdownExec
335
361
  end
336
362
  end
337
363
 
338
- required_lines
364
+ required = mdoc.collect_recursively_required_code(opts[:block_name], opts: opts)
365
+ read_required_blocks_from_temp_file + required[:code]
339
366
  end
340
367
 
341
368
  def cfile
342
- @cfile ||= CachedNestedFileReader.new(import_pattern: @options.fetch(:import_pattern))
369
+ @cfile ||= CachedNestedFileReader.new(
370
+ import_pattern: @options.fetch(:import_pattern)
371
+ )
343
372
  end
344
373
 
345
374
  EF_STDOUT = :stdout
346
375
  EF_STDERR = :stderr
347
376
  EF_STDIN = :stdin
348
377
 
349
- # Handles reading and processing lines from a given IO stream
350
- #
351
- # @param stream [IO] The IO stream to read from (e.g., stdout, stderr, stdin).
352
- # @param file_type [Symbol] The type of file to which the stream corresponds.
353
- def handle_stream(opts, stream, file_type, swap: false)
354
- Thread.new do
355
- until (line = stream.gets).nil?
356
- @execute_files[file_type] = @execute_files[file_type] + [line.strip]
357
- print line if opts[:output_stdout]
358
- yield line if block_given?
359
- end
360
- rescue IOError
361
- #d 'stdout IOError, thread killed, do nothing'
362
- end
363
- end
364
-
365
378
  # Existing command_execute method
366
379
  def command_execute(opts, command, args: [])
367
380
  @execute_files = Hash.new([])
@@ -404,6 +417,14 @@ module MarkdownExec
404
417
  fout "Error ENOENT: #{err.inspect}"
405
418
  end
406
419
 
420
+ def copy_to_clipboard(required_lines)
421
+ text = required_lines.flatten.join($INPUT_RECORD_SEPARATOR)
422
+ Clipboard.copy(text)
423
+ fout "Clipboard updated: #{required_lines.count} blocks," \
424
+ " #{required_lines.flatten.count} lines," \
425
+ " #{text.length} characters"
426
+ end
427
+
407
428
  def count_blocks_in_filename
408
429
  fenced_start_and_end_regex = Regexp.new @options[:fenced_start_and_end_regex]
409
430
  cnt = 0
@@ -413,6 +434,45 @@ module MarkdownExec
413
434
  cnt / 2
414
435
  end
415
436
 
437
+ def create_and_write_file_with_permissions(file_path, content, chmod_value)
438
+ dirname = File.dirname(file_path)
439
+ FileUtils.mkdir_p dirname
440
+ File.write(file_path, content)
441
+ return if chmod_value.zero?
442
+
443
+ File.chmod chmod_value, file_path
444
+ end
445
+
446
+ # Deletes a required temporary file specified by an environment variable.
447
+ # The function checks if the file exists before attempting to delete it.
448
+ # Clears the environment variable after deletion.
449
+ #
450
+ def delete_required_temp_file
451
+ temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
452
+
453
+ return if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
454
+
455
+ FileUtils.rm_f(temp_blocks_file_path)
456
+
457
+ clear_required_file
458
+ end
459
+
460
+ ## Determines the correct filename to use for searching files
461
+ #
462
+ def determine_filename(specified_filename: nil, specified_folder: nil, default_filename: nil,
463
+ default_folder: nil, filetree: nil)
464
+ if specified_filename&.present?
465
+ return specified_filename if specified_filename.start_with?('/')
466
+
467
+ File.join(specified_folder || default_folder, specified_filename)
468
+ elsif specified_folder&.present?
469
+ File.join(specified_folder,
470
+ filetree ? @options[:md_filename_match] : @options[:md_filename_glob])
471
+ else
472
+ File.join(default_folder, default_filename)
473
+ end
474
+ end
475
+
416
476
  # :reek:DuplicateMethodCall
417
477
  def display_required_code(opts, required_lines)
418
478
  frame = opts[:output_divider].send(opts[:output_divider_color].to_sym)
@@ -421,14 +481,41 @@ module MarkdownExec
421
481
  fout frame
422
482
  end
423
483
 
424
- # :reek:DuplicateMethodCall
425
- def exec_block(options, _block_name = '')
426
- options = calculated_options.merge(options)
427
- update_options options, over: false
484
+ def execute_approved_block(opts, required_lines)
485
+ write_command_file(opts, required_lines)
486
+ command_execute(
487
+ opts,
488
+ required_lines.flatten.join("\n"),
489
+ args: opts.fetch(:pass_args, [])
490
+ )
491
+ initialize_and_save_execution_output
492
+ output_execution_summary
493
+ output_execution_result
494
+ end
428
495
 
429
- # document and block reports
430
- #
431
- files = list_files_per_options(options)
496
+ # Reports and executes block logic
497
+ def execute_block_logic(files)
498
+ @options[:filename] = select_document_if_multiple(files)
499
+ select_approve_and_execute_block({
500
+ bash: true,
501
+ struct: true
502
+ })
503
+ end
504
+
505
+ ## Executes the block specified in the options
506
+ #
507
+ def execute_block_with_error_handling(rest)
508
+ finalize_cli_argument_processing(rest)
509
+ @options[:cli_rest] = rest
510
+ execute_code_block_based_on_options(@options)
511
+ rescue FileMissingError => err
512
+ puts "File missing: #{err}"
513
+ end
514
+
515
+ # Main method to execute a block based on options and block_name
516
+ def execute_code_block_based_on_options(options)
517
+ options = calculated_options.merge(options)
518
+ update_options(options, over: false)
432
519
 
433
520
  simple_commands = {
434
521
  doc_glob: -> { fout options[:md_filename_glob] },
@@ -459,27 +546,59 @@ module MarkdownExec
459
546
  tab_completions: -> { fout tab_completions },
460
547
  menu_export: -> { fout menu_export }
461
548
  }
549
+
550
+ return if execute_simple_commands(simple_commands)
551
+
552
+ files = prepare_file_list(options)
553
+ execute_block_logic(files)
554
+ return unless @options[:output_saved_script_filename]
555
+
556
+ fout "saved_filespec: #{@execute_script_filespec}"
557
+ rescue StandardError => err
558
+ warn(error = "ERROR ** MarkParse.execute_code_block_based_on_options(); #{err.inspect}")
559
+ binding.pry if $tap_enable
560
+ raise ArgumentError, error
561
+ end
562
+
563
+ # Executes command based on the provided option keys
564
+ def execute_simple_commands(simple_commands)
462
565
  simple_commands.each_key do |key|
463
566
  if @options[key]
464
567
  simple_commands[key].call
465
- return # rubocop:disable Lint/NonLocalExitFromIterator
568
+ return true
466
569
  end
467
570
  end
571
+ false
572
+ end
573
+
574
+ ##
575
+ # Determines the types of blocks to select based on the filter.
576
+ #
577
+ def filter_block_types
578
+ ## return type of blocks to select
579
+ #
580
+ %i[blocks line]
581
+ end
468
582
 
469
- # process
583
+ ## post-parse options configuration
584
+ #
585
+ def finalize_cli_argument_processing(rest)
586
+ ## position 0: file or folder (optional)
470
587
  #
471
- @options[:filename] = select_md_file(files)
472
- select_approve_and_execute_block({
473
- bash: true,
474
- struct: true
475
- })
476
- return unless @options[:output_saved_script_filename]
588
+ if (pos = rest.shift)&.present?
589
+ if Dir.exist?(pos)
590
+ @options[:path] = pos
591
+ elsif File.exist?(pos)
592
+ @options[:filename] = pos
593
+ else
594
+ raise FileMissingError, pos, caller
595
+ end
596
+ end
477
597
 
478
- fout "saved_filespec: #{@execute_script_filespec}"
479
- rescue StandardError => err
480
- warn(error = "ERROR ** MarkParse.exec_block(); #{err.inspect}")
481
- binding.pry if $tap_enable
482
- raise ArgumentError, error
598
+ ## position 1: block name (optional)
599
+ #
600
+ block_name = rest.shift
601
+ @options[:block_name] = block_name if block_name.present?
483
602
  end
484
603
 
485
604
  ## summarize blocks
@@ -495,9 +614,9 @@ module MarkdownExec
495
614
  else
496
615
  fcb.title
497
616
  end
498
- bm = option_match_groups(titlexcall, opts[:block_name_match])
499
- fcb.stdin = option_match_groups(titlexcall, opts[:block_stdin_scan])
500
- fcb.stdout = option_match_groups(titlexcall, opts[:block_stdout_scan])
617
+ bm = extract_named_captures_from_option(titlexcall, opts[:block_name_match])
618
+ fcb.stdin = extract_named_captures_from_option(titlexcall, opts[:block_stdin_scan])
619
+ fcb.stdout = extract_named_captures_from_option(titlexcall, opts[:block_stdout_scan])
501
620
 
502
621
  shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
503
622
  fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall
@@ -509,218 +628,268 @@ module MarkdownExec
509
628
  fcb
510
629
  end
511
630
 
512
- # :reek:DuplicateMethodCall
513
- # :reek:LongYieldList
514
- # :reek:NestedIterators
515
- #---
631
+ ##
632
+ # Handles errors that occur during the block listing process.
633
+ #
634
+ def handle_error(err)
635
+ warn(error = "ERROR ** MarkParse.list_blocks_in_file(); #{err.inspect}")
636
+ warn(caller[0..4])
637
+ raise StandardError, error
638
+ end
516
639
 
517
- def iter_blocks_in_file(opts = {}, &block)
518
- unless opts[:filename]&.present?
519
- fout 'No blocks found.'
520
- return
521
- end
640
+ # Handles the link-back operation.
641
+ #
642
+ # @param opts [Hash] Configuration options hash.
643
+ # @return [Array<Symbol, String>] A tuple containing a LOAD_FILE flag and an empty string.
644
+ def handle_back_link(opts)
645
+ history_state_pop(opts)
646
+ [LOAD_FILE, '']
647
+ end
522
648
 
523
- unless File.exist? opts[:filename]
524
- fout 'Document is missing.'
525
- return
649
+ # Handles the execution and display of remainder blocks from a selected menu item.
650
+ #
651
+ # @param mdoc [Object] Document object containing code blocks.
652
+ # @param opts [Hash] Configuration options hash.
653
+ # @param selected [Hash] Selected item from the menu.
654
+ # @return [Array<Symbol, String>] A tuple containing a LOAD_FILE flag and an empty string.
655
+ # @note The function can prompt the user for approval before executing code if opts[:user_must_approve] is true.
656
+ def handle_remainder_blocks(mdoc, opts, selected)
657
+ required_lines = collect_required_code_lines(mdoc, selected, opts: opts)
658
+ if opts[:output_script] || opts[:user_must_approve]
659
+ display_required_code(opts, required_lines)
526
660
  end
661
+ allow = opts[:user_must_approve] ? prompt_for_user_approval(opts, required_lines) : true
662
+ opts[:ir_approve] = allow
663
+ execute_approved_block(opts, required_lines) if opts[:ir_approve]
527
664
 
528
- fenced_start_and_end_regex = Regexp.new opts[:fenced_start_and_end_regex]
529
- fenced_start_extended_regex = Regexp.new opts[:fenced_start_extended_regex]
530
- fcb = FCB.new
531
- in_fenced_block = false
532
- headings = []
665
+ [!LOAD_FILE, '']
666
+ end
533
667
 
534
- ## get type of messages to select
535
- #
536
- selected_messages = yield :filter
668
+ # Handles the link-shell operation.
669
+ #
670
+ # @param opts [Hash] Configuration options hash.
671
+ # @param body [Array<String>] The body content.
672
+ # @param mdoc [Object] Document object containing code blocks.
673
+ # @return [Array<Symbol, String>] A tuple containing a LOAD_FILE flag and a block name.
674
+ def handle_shell_link(opts, body, mdoc)
675
+ data = body.present? ? YAML.load(body.join("\n")) : {}
676
+ data_file = data.fetch('file', nil)
677
+ return [!LOAD_FILE, ''] unless data_file
537
678
 
538
- cfile.readlines(opts[:filename]).each.with_index do |line, _line_num|
539
- continue unless line
540
- headings = update_headings(line, headings, opts) if opts[:menu_blocks_with_headings]
541
-
542
- if line.match(fenced_start_and_end_regex)
543
- if in_fenced_block
544
- process_fenced_block(fcb, opts, selected_messages, &block)
545
- in_fenced_block = false
546
- else
547
- fcb = start_fenced_block(opts, line, headings, fenced_start_extended_regex)
548
- in_fenced_block = true
549
- end
550
- elsif in_fenced_block && fcb.body
551
- dp 'append line to fcb body'
552
- fcb.body += [line.chomp]
553
- else
554
- process_line(line, opts, selected_messages, &block)
555
- end
679
+ history_state_push(mdoc, data_file, opts)
680
+
681
+ data.fetch('vars', []).each do |var|
682
+ ENV[var[0]] = var[1].to_s
556
683
  end
684
+
685
+ [LOAD_FILE, data.fetch('block', '')]
557
686
  end
558
687
 
559
- def start_fenced_block(opts, line, headings, fenced_start_extended_regex)
560
- fcb_title_groups = line.match(fenced_start_extended_regex).named_captures.sym_keys
561
- fcb = FCB.new
562
- fcb.headings = headings
563
- fcb.oname = fcb.dname = fcb_title_groups.fetch(:name, '')
564
- fcb.shell = fcb_title_groups.fetch(:shell, '')
565
- fcb.title = fcb_title_groups.fetch(:name, '')
566
-
567
- # selected fcb
568
- fcb.body = []
688
+ # Handles options for the shell.
689
+ #
690
+ # @param opts [Hash] Configuration options hash.
691
+ # @param selected [Hash] Selected item from the menu.
692
+ # @return [Array<Symbol, String>] A tuple containing a NOT_LOAD_FILE flag and an empty string.
693
+ def handle_shell_opts(opts, selected)
694
+ data = YAML.load(selected[:body].join("\n"))
695
+ data.each_key do |key|
696
+ opts[key.to_sym] = value = data[key].to_s
697
+ next unless opts[:menu_opts_set_format].present?
569
698
 
570
- rest = fcb_title_groups.fetch(:rest, '')
571
- fcb.reqs, fcb.wraps =
572
- split_array(rest.scan(/\+[^\s]+/).map { |req| req[1..-1] }) do |name|
573
- !name.match(Regexp.new(opts[:block_name_wrapper_match]))
699
+ print format(
700
+ opts[:menu_opts_set_format],
701
+ { key: key,
702
+ value: value }
703
+ ).send(opts[:menu_opts_set_color].to_sym)
574
704
  end
575
- fcb.call = rest.match(Regexp.new(opts[:block_calls_scan]))&.to_a&.first
576
- fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
577
- tn.named_captures.sym_keys
578
- end
579
- fcb.stdout = if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
580
- tn.named_captures.sym_keys
581
- end
582
- fcb
705
+ [!LOAD_FILE, '']
583
706
  end
584
707
 
585
- def process_fenced_block(fcb, opts, selected_messages, &block)
586
- fcb.oname = fcb.dname = fcb.title || ''
587
- return unless fcb.body
588
-
589
- set_fcb_title(fcb)
590
-
591
- if block &&
592
- selected_messages.include?(:blocks) &&
593
- Filter.fcb_select?(opts, fcb)
594
- block.call :blocks, fcb
708
+ # Handles reading and processing lines from a given IO stream
709
+ #
710
+ # @param stream [IO] The IO stream to read from (e.g., stdout, stderr, stdin).
711
+ # @param file_type [Symbol] The type of file to which the stream corresponds.
712
+ def handle_stream(opts, stream, file_type, swap: false)
713
+ Thread.new do
714
+ until (line = stream.gets).nil?
715
+ @execute_files[file_type] = @execute_files[file_type] + [line.strip]
716
+ print line if opts[:output_stdout]
717
+ yield line if block_given?
718
+ end
719
+ rescue IOError
720
+ #d 'stdout IOError, thread killed, do nothing'
595
721
  end
596
722
  end
597
723
 
598
- def process_line(line, _opts, selected_messages, &block)
599
- return unless block && selected_messages.include?(:line)
724
+ def history_state_exist?
725
+ history = ENV.fetch(MDE_HISTORY_ENV_NAME, '')
726
+ history.present? ? history : nil
727
+ end
600
728
 
601
- # dp 'text outside of fcb'
602
- fcb = FCB.new
603
- fcb.body = [line]
604
- block.call(:line, fcb)
729
+ def history_state_partition(opts)
730
+ unit, rest = StringUtil.partition_at_first(
731
+ ENV.fetch(MDE_HISTORY_ENV_NAME, ''),
732
+ opts[:history_document_separator]
733
+ )
734
+ { unit: unit, rest: rest }.tap_inspect
605
735
  end
606
736
 
607
- # set the title of an FCB object based on its body if it is nil or empty
608
- def set_fcb_title(fcb)
609
- return unless fcb.title.nil? || fcb.title.empty?
737
+ def history_state_pop(opts)
738
+ state = history_state_partition(opts)
739
+ opts[:filename] = state[:unit]
740
+ ENV[MDE_HISTORY_ENV_NAME] = state[:rest]
741
+ delete_required_temp_file
742
+ end
610
743
 
611
- fcb.title = (fcb&.body || []).join(' ').gsub(/ +/, ' ')[0..64]
744
+ def history_state_push(mdoc, data_file, opts)
745
+ [data_file, opts[:block_name]].tap_inspect 'filename, blockname'
746
+ new_history = opts[:filename] +
747
+ opts[:history_document_separator] +
748
+ ENV.fetch(MDE_HISTORY_ENV_NAME, '')
749
+ opts[:filename] = data_file
750
+ write_required_blocks_to_temp_file(mdoc, opts[:block_name], opts)
751
+ ENV[MDE_HISTORY_ENV_NAME] = new_history
612
752
  end
613
753
 
614
- def split_array(arr)
615
- true_list = []
616
- false_list = []
754
+ ## Sets up the options and returns the parsed arguments
755
+ #
756
+ def initialize_and_parse_cli_options
757
+ @options = base_options
758
+ read_configuration_file!(@options, ".#{MarkdownExec::APP_NAME.downcase}.yml")
617
759
 
618
- arr.each do |element|
619
- if yield(element)
620
- true_list << element
621
- else
622
- false_list << element
760
+ @option_parser = OptionParser.new do |opts|
761
+ executable_name = File.basename($PROGRAM_NAME)
762
+ opts.banner = [
763
+ "#{MarkdownExec::APP_NAME}" \
764
+ " - #{MarkdownExec::APP_DESC} (#{MarkdownExec::VERSION})",
765
+ "Usage: #{executable_name} [(path | filename [block_name])] [options]"
766
+ ].join("\n")
767
+
768
+ menu_iter do |item|
769
+ menu_option_append opts, @options, item
623
770
  end
624
771
  end
772
+ @option_parser.load
773
+ @option_parser.environment
625
774
 
626
- [true_list, false_list]
775
+ rest = @option_parser.parse!(arguments_for_mde)
776
+ @options[:pass_args] = ARGV[rest.count + 1..]
777
+
778
+ rest
627
779
  end
628
780
 
629
- def update_headings(line, headings, opts)
630
- if (lm = line.match(Regexp.new(opts[:heading3_match])))
631
- [headings[0], headings[1], lm[:name]]
632
- elsif (lm = line.match(Regexp.new(opts[:heading2_match])))
633
- [headings[0], lm[:name]]
634
- elsif (lm = line.match(Regexp.new(opts[:heading1_match])))
635
- [lm[:name]]
636
- else
637
- headings
781
+ def initialize_and_save_execution_output
782
+ return unless @options[:save_execution_output]
783
+
784
+ @options[:logged_stdout_filename] =
785
+ SavedAsset.stdout_name(blockname: @options[:block_name],
786
+ filename: File.basename(@options[:filename], '.*'),
787
+ prefix: @options[:logged_stdout_filename_prefix],
788
+ time: Time.now.utc)
789
+
790
+ @logged_stdout_filespec =
791
+ @options[:logged_stdout_filespec] =
792
+ File.join @options[:saved_stdout_folder],
793
+ @options[:logged_stdout_filename]
794
+ @logged_stdout_filespec = @options[:logged_stdout_filespec]
795
+ write_execution_output_to_file
796
+ end
797
+
798
+ # Initializes variables for regex and other states
799
+ def initialize_state(opts)
800
+ {
801
+ fenced_start_and_end_regex: Regexp.new(opts[:fenced_start_and_end_regex]),
802
+ fenced_start_extended_regex: Regexp.new(opts[:fenced_start_extended_regex]),
803
+ fcb: FCB.new,
804
+ in_fenced_block: false,
805
+ headings: []
806
+ }
807
+ end
808
+
809
+ # Main function to iterate through blocks in file
810
+ def iter_blocks_in_file(opts = {}, &block)
811
+ return unless check_file_existence(opts[:filename])
812
+
813
+ state = initialize_state(opts)
814
+
815
+ # get type of messages to select
816
+ selected_messages = yield :filter
817
+
818
+ cfile.readlines(opts[:filename]).each do |line|
819
+ next unless line
820
+
821
+ update_line_and_block_state(line, state, opts, selected_messages, &block)
638
822
  end
639
823
  end
640
824
 
641
- # return body, title if option.struct
642
- # return body if not struct
825
+ ##
826
+ # Returns a list of blocks in a given file, including dividers, tasks, and other types of blocks.
827
+ # The list can be customized via call_options and options_block.
828
+ #
829
+ # @param call_options [Hash] Options passed as an argument.
830
+ # @param options_block [Proc] Block for dynamic option manipulation.
831
+ # @return [Array<FCB>] An array of FCB objects representing the blocks.
643
832
  #
644
833
  def list_blocks_in_file(call_options = {}, &options_block)
645
834
  opts = optsmerge(call_options, options_block)
646
835
  use_chrome = !opts[:no_chrome]
647
836
 
648
837
  blocks = []
649
- if opts[:menu_initial_divider].present? && use_chrome
650
- blocks.push FCB.new({
651
- # name: '',
652
- chrome: true,
653
- dname: format(
654
- opts[:menu_divider_format],
655
- opts[:menu_initial_divider]
656
- ).send(opts[:menu_divider_color].to_sym),
657
- oname: opts[:menu_initial_divider],
658
- disabled: '' # __LINE__.to_s
659
- })
660
- end
838
+ append_initial_divider(blocks, opts) if use_chrome
661
839
 
662
840
  iter_blocks_in_file(opts) do |btype, fcb|
663
841
  case btype
664
842
  when :filter
665
- ## return type of blocks to select
666
- #
667
- %i[blocks line]
668
-
843
+ filter_block_types
669
844
  when :line
670
- ## convert line to block
671
- #
672
- if opts[:menu_divider_match].present? &&
673
- (mbody = fcb.body[0].match opts[:menu_divider_match])
674
- if use_chrome
675
- blocks.push FCB.new(
676
- { chrome: true,
677
- disabled: '',
678
- dname: format(opts[:menu_divider_format],
679
- mbody[:name]).send(opts[:menu_divider_color].to_sym),
680
- oname: mbody[:name] }
681
- )
682
- end
683
- elsif opts[:menu_task_match].present? &&
684
- (fcb.body[0].match opts[:menu_task_match])
685
- if use_chrome
686
- blocks.push FCB.new(
687
- { chrome: true,
688
- disabled: '',
689
- dname: format(
690
- opts[:menu_task_format],
691
- $~.named_captures.transform_keys(&:to_sym)
692
- ).send(opts[:menu_task_color].to_sym),
693
- oname: format(
694
- opts[:menu_task_format],
695
- $~.named_captures.transform_keys(&:to_sym)
696
- ) }
697
- )
698
- end
699
- else
700
- # line not added
701
- end
845
+ process_line_blocks(blocks, fcb, opts, use_chrome)
702
846
  when :blocks
703
- ## enhance fcb with block summary
704
- #
705
- blocks.push get_block_summary(opts, fcb) ### if Filter.fcb_select? opts, fcb
847
+ append_block_summary(blocks, fcb, opts)
706
848
  end
707
849
  end
708
850
 
709
- if opts[:menu_divider_format].present? && opts[:menu_final_divider].present? && use_chrome && use_chrome
710
- blocks.push FCB.new(
711
- { chrome: true,
712
- disabled: '',
713
- dname: format(opts[:menu_divider_format],
714
- opts[:menu_final_divider])
715
- .send(opts[:menu_divider_color].to_sym),
716
- oname: opts[:menu_final_divider] }
717
- )
718
- end
851
+ append_final_divider(blocks, opts) if use_chrome
719
852
  blocks
720
853
  rescue StandardError => err
721
- warn(error = "ERROR ** MarkParse.list_blocks_in_file(); #{err.inspect}")
722
- warn(caller[0..4])
723
- raise StandardError, error
854
+ handle_error(err)
855
+ end
856
+
857
+ ##
858
+ # Processes lines within the file and converts them into blocks if they match certain criteria.
859
+ #
860
+ def process_line_blocks(blocks, fcb, opts, use_chrome)
861
+ ## convert line to block
862
+ #
863
+ if opts[:menu_divider_match].present? &&
864
+ (mbody = fcb.body[0].match opts[:menu_divider_match])
865
+ if use_chrome
866
+ blocks.push FCB.new(
867
+ { chrome: true,
868
+ disabled: '',
869
+ dname: format(opts[:menu_divider_format],
870
+ mbody[:name]).send(opts[:menu_divider_color].to_sym),
871
+ oname: mbody[:name] }
872
+ )
873
+ end
874
+ elsif opts[:menu_task_match].present? &&
875
+ (fcb.body[0].match opts[:menu_task_match])
876
+ if use_chrome
877
+ blocks.push FCB.new(
878
+ { chrome: true,
879
+ disabled: '',
880
+ dname: format(
881
+ opts[:menu_task_format],
882
+ $~.named_captures.transform_keys(&:to_sym)
883
+ ).send(opts[:menu_task_color].to_sym),
884
+ oname: format(
885
+ opts[:menu_task_format],
886
+ $~.named_captures.transform_keys(&:to_sym)
887
+ ) }
888
+ )
889
+ end
890
+ else
891
+ # line not added
892
+ end
724
893
  end
725
894
 
726
895
  def list_default_env
@@ -747,39 +916,22 @@ module MarkdownExec
747
916
 
748
917
  def list_files_per_options(options)
749
918
  list_files_specified(
750
- specified_filename: options[:filename]&.present? ? options[:filename] : nil,
751
- specified_folder: options[:path],
752
- default_filename: 'README.md',
753
- default_folder: '.'
919
+ determine_filename(
920
+ specified_filename: options[:filename]&.present? ? options[:filename] : nil,
921
+ specified_folder: options[:path],
922
+ default_filename: 'README.md',
923
+ default_folder: '.'
924
+ )
754
925
  )
755
926
  end
756
927
 
757
- # :reek:LongParameterList
758
- def list_files_specified(specified_filename: nil, specified_folder: nil,
759
- default_filename: nil, default_folder: nil, filetree: nil)
760
- fn = File.join(if specified_filename&.present?
761
- if specified_filename.start_with? '/'
762
- [specified_filename]
763
- elsif specified_folder&.present?
764
- [specified_folder, specified_filename]
765
- else
766
- [default_folder, specified_filename]
767
- end
768
- elsif specified_folder&.present?
769
- if filetree
770
- [specified_folder, @options[:md_filename_match]]
771
- else
772
- [specified_folder, @options[:md_filename_glob]]
773
- end
774
- else
775
- [default_folder, default_filename]
776
- end)
777
- if filetree
778
- filetree.select do |filename|
779
- filename == fn || filename.match(/^#{fn}$/) || filename.match(%r{^#{fn}/.+$})
780
- end
781
- else
782
- Dir.glob(fn)
928
+ ## Searches for files based on the specified or default filenames and folders
929
+ #
930
+ def list_files_specified(fn, filetree = nil)
931
+ return Dir.glob(fn) unless filetree
932
+
933
+ filetree.select do |filename|
934
+ filename == fn || filename.match(/^#{fn}$/) || filename.match(%r{^#{fn}/.+$})
783
935
  end
784
936
  end
785
937
 
@@ -788,17 +940,6 @@ module MarkdownExec
788
940
  @options[:md_filename_glob]))
789
941
  end
790
942
 
791
- def blocks_per_opts(blocks, opts)
792
- if opts[:struct]
793
- blocks
794
- else
795
- # blocks.map(&:name)
796
- blocks.map do |block|
797
- block.fetch(:text, nil) || block.oname
798
- end
799
- end.compact.reject(&:empty?)
800
- end
801
-
802
943
  ## output type (body string or full object) per option struct and bash
803
944
  #
804
945
  def list_named_blocks_in_file(call_options = {}, &options_block)
@@ -811,6 +952,17 @@ module MarkdownExec
811
952
  blocks_per_opts(blocks, opts)
812
953
  end
813
954
 
955
+ ## Handles the file loading and returns the blocks in the file and MDoc instance
956
+ #
957
+ def load_file_and_prepare_menu(opts)
958
+ blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
959
+ mdoc = MDoc.new(blocks_in_file) do |nopts|
960
+ opts.merge!(nopts)
961
+ end
962
+ blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
963
+ [blocks_in_file, blocks_menu, mdoc]
964
+ end
965
+
814
966
  def make_block_labels(call_options = {})
815
967
  opts = options.merge(call_options)
816
968
  list_blocks_in_file(opts).map do |fcb|
@@ -826,6 +978,32 @@ module MarkdownExec
826
978
  end.compact
827
979
  end
828
980
 
981
+ def menu_export(data = menu_for_optparse)
982
+ data.map do |item|
983
+ item.delete(:procname)
984
+ item
985
+ end.to_yaml
986
+ end
987
+
988
+ def menu_for_blocks(menu_options)
989
+ options = calculated_options.merge menu_options
990
+ menu = []
991
+ iter_blocks_in_file(options) do |btype, fcb|
992
+ case btype
993
+ when :filter
994
+ %i[blocks line]
995
+ when :line
996
+ if options[:menu_divider_match] &&
997
+ (mbody = fcb.body[0].match(options[:menu_divider_match]))
998
+ menu.push FCB.new({ dname: mbody[:name], oname: mbody[:name], disabled: '' })
999
+ end
1000
+ when :blocks
1001
+ menu += [fcb.oname]
1002
+ end
1003
+ end
1004
+ menu
1005
+ end
1006
+
829
1007
  # :reek:DuplicateMethodCall
830
1008
  # :reek:NestedIterators
831
1009
  def menu_for_optparse
@@ -849,11 +1027,11 @@ module MarkdownExec
849
1027
  }
850
1028
  when 'path'
851
1029
  lambda { |value|
852
- read_configuration_file! options, value
1030
+ read_configuration_file!(options, value)
853
1031
  }
854
1032
  when 'show_config'
855
1033
  lambda { |_|
856
- options_finalize options
1034
+ finalize_cli_argument_processing(options)
857
1035
  fout options.sort_by_key.to_yaml
858
1036
  }
859
1037
  when 'val_as_bool'
@@ -877,33 +1055,14 @@ module MarkdownExec
877
1055
  end
878
1056
  end
879
1057
 
880
- def menu_for_blocks(menu_options)
881
- options = calculated_options.merge menu_options
882
- menu = []
883
- iter_blocks_in_file(options) do |btype, fcb|
884
- case btype
885
- when :filter
886
- %i[blocks line]
887
- when :line
888
- if options[:menu_divider_match] &&
889
- (mbody = fcb.body[0].match(options[:menu_divider_match]))
890
- menu.push FCB.new({ dname: mbody[:name], oname: mbody[:name], disabled: '' })
891
- end
892
- when :blocks
893
- menu += [fcb.oname]
894
- end
895
- end
896
- menu
1058
+ def menu_help
1059
+ @option_parser.help
897
1060
  end
898
1061
 
899
1062
  def menu_iter(data = menu_for_optparse, &block)
900
1063
  data.map(&block)
901
1064
  end
902
1065
 
903
- def menu_help
904
- @option_parser.help
905
- end
906
-
907
1066
  def menu_option_append(opts, options, item)
908
1067
  return unless item[:long_name].present? || item[:short_name].present?
909
1068
 
@@ -931,25 +1090,13 @@ module MarkdownExec
931
1090
  ].compact)
932
1091
  end
933
1092
 
934
- ## post-parse options configuration
935
- #
936
- def options_finalize(rest)
937
- ## position 0: file or folder (optional)
938
- #
939
- if (pos = rest.shift)&.present?
940
- if Dir.exist?(pos)
941
- @options[:path] = pos
942
- elsif File.exist?(pos)
943
- @options[:filename] = pos
944
- else
945
- raise FileMissingError, pos, caller
946
- end
1093
+ def next_block_name_from_command_line_arguments(opts)
1094
+ if opts[:cli_rest].present?
1095
+ opts[:block_name] = opts[:cli_rest].pop
1096
+ false # repeat_menu
1097
+ else
1098
+ true # repeat_menu
947
1099
  end
948
-
949
- ## position 1: block name (optional)
950
- #
951
- block_name = rest.shift
952
- @options[:block_name] = block_name if block_name.present?
953
1100
  end
954
1101
 
955
1102
  # :reek:ControlParameter
@@ -995,63 +1142,112 @@ module MarkdownExec
995
1142
  }
996
1143
  end
997
1144
 
998
- ## insert back option at head or tail
999
- #
1000
- ## Adds a back option at the head or tail of a menu
1145
+ # Prepare the blocks menu by adding labels and other necessary details.
1001
1146
  #
1002
- def prompt_menu_add_back(items, label = BACK_OPTION)
1003
- return items unless @options[:menu_with_back]
1147
+ # @param blocks_in_file [Array<Hash>] The list of blocks from the file.
1148
+ # @param opts [Hash] The options hash.
1149
+ # @return [Array<Hash>] The updated blocks menu.
1150
+ def prepare_blocks_menu(blocks_in_file, opts)
1151
+ # next if fcb.fetch(:disabled, false)
1152
+ # next unless fcb.fetch(:name, '').present?
1153
+ blocks_in_file.map do |fcb|
1154
+ fcb.merge!(
1155
+ name: fcb.dname,
1156
+ label: BlockLabel.make(
1157
+ body: fcb[:body],
1158
+ filename: opts[:filename],
1159
+ headings: fcb.fetch(:headings, []),
1160
+ menu_blocks_with_docname: opts[:menu_blocks_with_docname],
1161
+ menu_blocks_with_headings: opts[:menu_blocks_with_headings],
1162
+ text: fcb[:text],
1163
+ title: fcb[:title]
1164
+ )
1165
+ )
1166
+ fcb.to_h
1167
+ end.compact
1168
+ end
1004
1169
 
1005
- history = ENV.fetch('MDE_MENU_HISTORY', '')
1006
- return items unless history.present?
1170
+ # Prepares and fetches file listings
1171
+ def prepare_file_list(options)
1172
+ list_files_per_options(options)
1173
+ end
1007
1174
 
1008
- @hs_curr, @hs_rest = split_string_on_first_char(
1009
- history,
1010
- @options[:history_document_separator]
1011
- )
1175
+ def process_fenced_block(fcb, opts, selected_messages, &block)
1176
+ fcb.oname = fcb.dname = fcb.title || ''
1177
+ return unless fcb.body
1012
1178
 
1013
- @options[:menu_back_at_top] ? [label] + items : items + [label]
1179
+ set_fcb_title(fcb)
1180
+
1181
+ if block &&
1182
+ selected_messages.include?(:blocks) &&
1183
+ Filter.fcb_select?(opts, fcb)
1184
+ block.call :blocks, fcb
1185
+ end
1014
1186
  end
1015
1187
 
1016
- ## insert exit option at head or tail
1188
+ def process_line(line, _opts, selected_messages, &block)
1189
+ return unless block && selected_messages.include?(:line)
1190
+
1191
+ # dp 'text outside of fcb'
1192
+ fcb = FCB.new
1193
+ fcb.body = [line]
1194
+ block.call(:line, fcb)
1195
+ end
1196
+
1197
+ ##
1198
+ # Presents a menu to the user for approving an action and performs additional tasks based on the selection.
1199
+ # The function provides options for approval, rejection, copying data to clipboard, or saving data to a file.
1017
1200
  #
1018
- def prompt_menu_add_exit(items, label = EXIT_OPTION)
1019
- if @options[:menu_exit_at_top]
1020
- (@options[:menu_with_exit] ? [label] : []) + items
1021
- else
1022
- items + (@options[:menu_with_exit] ? [label] : [])
1201
+ # @param opts [Hash] A hash containing various options for the menu.
1202
+ # @param required_lines [Array<String>] Lines of text or code that are subject to user approval.
1203
+ #
1204
+ # @option opts [String] :prompt_approve_block Prompt text for the approval menu.
1205
+ # @option opts [String] :prompt_yes Text for the 'Yes' choice in the menu.
1206
+ # @option opts [String] :prompt_no Text for the 'No' choice in the menu.
1207
+ # @option opts [String] :prompt_script_to_clipboard Text for the 'Copy to Clipboard' choice in the menu.
1208
+ # @option opts [String] :prompt_save_script Text for the 'Save to File' choice in the menu.
1209
+ #
1210
+ # @return [Boolean] Returns true if the user approves (selects 'Yes'), false otherwise.
1211
+ ##
1212
+ def prompt_for_user_approval(opts, required_lines)
1213
+ # Present a selection menu for user approval.
1214
+ sel = @prompt.select(opts[:prompt_approve_block], filter: true) do |menu|
1215
+ menu.default 1
1216
+ menu.choice opts[:prompt_yes], 1
1217
+ menu.choice opts[:prompt_no], 2
1218
+ menu.choice opts[:prompt_script_to_clipboard], 3
1219
+ menu.choice opts[:prompt_save_script], 4
1023
1220
  end
1221
+
1222
+ if sel == 3
1223
+ copy_to_clipboard(required_lines)
1224
+ elsif sel == 4
1225
+ save_to_file(opts, required_lines)
1226
+ end
1227
+
1228
+ sel == 1
1024
1229
  end
1025
1230
 
1026
- ## tty prompt to select
1027
- # insert exit option at head or tail
1028
- # return selected option or nil
1231
+ ## insert back option at head or tail
1029
1232
  #
1030
- def prompt_with_quit(prompt_text, items, opts = {})
1031
- obj = prompt_with_quit2(prompt_text, items, opts)
1032
- if obj.fetch(:option, nil)
1033
- nil
1034
- else
1035
- obj[:selected]
1036
- end
1233
+ ## Adds a back option at the head or tail of a menu
1234
+ #
1235
+ def prompt_menu_add_back(items, label)
1236
+ return items unless @options[:menu_with_back] && history_state_exist?
1237
+
1238
+ state = history_state_partition(@options)
1239
+ @hs_curr = state[:unit]
1240
+ @hs_rest = state[:rest]
1241
+ @options[:menu_back_at_top] ? [label] + items : items + [label]
1037
1242
  end
1038
1243
 
1039
- ## tty prompt to select
1040
- # insert exit option at head or tail
1041
- # return option:, selected option:
1244
+ ## insert exit option at head or tail
1042
1245
  #
1043
- def prompt_with_quit2(prompt_text, items, opts = {})
1044
- sel = @prompt.select(prompt_text,
1045
- prompt_menu_add_exit(
1046
- prompt_menu_add_back(items)
1047
- ),
1048
- opts.merge(filter: true))
1049
- if sel == BACK_OPTION
1050
- { option: sel, curr: @hs_curr, rest: @hs_rest }
1051
- elsif sel == EXIT_OPTION
1052
- { option: sel }
1246
+ def prompt_menu_add_exit(items, label)
1247
+ if @options[:menu_exit_at_top]
1248
+ (@options[:menu_with_exit] ? [label] : []) + items
1053
1249
  else
1054
- { selected: sel }
1250
+ items + (@options[:menu_with_exit] ? [label] : [])
1055
1251
  end
1056
1252
  end
1057
1253
 
@@ -1063,54 +1259,33 @@ module MarkdownExec
1063
1259
  .transform_keys(&:to_sym))
1064
1260
  end
1065
1261
 
1066
- # :reek:NestedIterators
1067
- def run
1068
- @options = base_options
1262
+ # Reads required code blocks from a temporary file specified by an environment variable.
1263
+ #
1264
+ # @return [Array<String>] An array containing the lines read from the temporary file.
1265
+ # @note Relies on the 'MDE_LINK_REQUIRED_FILE' environment variable to locate the file.
1266
+ def read_required_blocks_from_temp_file
1267
+ temp_blocks = []
1069
1268
 
1070
- read_configuration_file! @options,
1071
- ".#{MarkdownExec::APP_NAME.downcase}.yml"
1269
+ temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
1270
+ return temp_blocks if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
1072
1271
 
1073
- @option_parser = option_parser = OptionParser.new do |opts|
1074
- executable_name = File.basename($PROGRAM_NAME)
1075
- opts.banner = [
1076
- "#{MarkdownExec::APP_NAME}" \
1077
- " - #{MarkdownExec::APP_DESC} (#{MarkdownExec::VERSION})",
1078
- "Usage: #{executable_name} [(path | filename [block_name])] [options]"
1079
- ].join("\n")
1080
-
1081
- menu_iter do |item|
1082
- menu_option_append opts, options, item
1083
- end
1272
+ if File.exist?(temp_blocks_file_path)
1273
+ temp_blocks = File.readlines(temp_blocks_file_path, chomp: true)
1084
1274
  end
1085
- option_parser.load
1086
- option_parser.environment
1087
- rest = option_parser.parse!(arguments_for_mde) # (into: options)
1088
1275
 
1089
- # pass through arguments excluded from OptionParser with `--`
1090
- @options[:pass_args] = ARGV[rest.count + 1..]
1276
+ temp_blocks
1277
+ end
1091
1278
 
1092
- begin
1093
- options_finalize rest
1094
- exec_block options, options[:block_name]
1095
- rescue FileMissingError => err
1096
- puts "File missing: #{err}"
1097
- end
1279
+ def run
1280
+ clear_required_file
1281
+ execute_block_with_error_handling(initialize_and_parse_cli_options)
1282
+ delete_required_temp_file
1098
1283
  rescue StandardError => err
1099
1284
  warn(error = "ERROR ** MarkParse.run(); #{err.inspect}")
1100
1285
  binding.pry if $tap_enable
1101
1286
  raise ArgumentError, error
1102
1287
  end
1103
1288
 
1104
- def saved_name_split(name)
1105
- # rubocop:disable Layout/LineLength
1106
- mf = /#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_,_(?<block>.+)\.sh/.match name
1107
- # rubocop:enable Layout/LineLength
1108
- return unless mf
1109
-
1110
- @options[:block_name] = mf[:block]
1111
- @options[:filename] = mf[:file].gsub(FNR12, FNR11)
1112
- end
1113
-
1114
1289
  def run_last_script
1115
1290
  filename = SavedFilesMatcher.most_recent(@options[:saved_script_folder],
1116
1291
  @options[:saved_script_glob])
@@ -1121,55 +1296,20 @@ module MarkdownExec
1121
1296
  select_approve_and_execute_block({})
1122
1297
  end
1123
1298
 
1124
- def save_execution_output
1125
- return unless @options[:save_execution_output]
1126
-
1127
- @options[:logged_stdout_filename] =
1128
- SavedAsset.stdout_name(blockname: @options[:block_name],
1129
- filename: File.basename(@options[:filename], '.*'),
1130
- prefix: @options[:logged_stdout_filename_prefix],
1131
- time: Time.now.utc)
1132
-
1133
- @options[:logged_stdout_filespec] =
1134
- File.join @options[:saved_stdout_folder],
1135
- @options[:logged_stdout_filename]
1136
- @logged_stdout_filespec = @options[:logged_stdout_filespec]
1137
- (dirname = File.dirname(@options[:logged_stdout_filespec]))
1138
- FileUtils.mkdir_p dirname
1139
-
1140
- ol = ["-STDOUT-\n"]
1141
- ol += @execute_files&.fetch(EF_STDOUT, [])
1142
- ol += ["\n-STDERR-\n"]
1143
- ol += @execute_files&.fetch(EF_STDERR, [])
1144
- ol += ["\n-STDIN-\n"]
1145
- ol += @execute_files&.fetch(EF_STDIN, [])
1146
- ol += ["\n"]
1147
- File.write(@options[:logged_stdout_filespec], ol.join)
1299
+ def save_to_file(opts, required_lines)
1300
+ write_command_file(opts.merge(save_executed_script: true),
1301
+ required_lines)
1302
+ fout "File saved: #{@options[:saved_filespec]}"
1148
1303
  end
1149
1304
 
1150
- # Prepare the blocks menu by adding labels and other necessary details.
1151
- #
1152
- # @param blocks_in_file [Array<Hash>] The list of blocks from the file.
1153
- # @param opts [Hash] The options hash.
1154
- # @return [Array<Hash>] The updated blocks menu.
1155
- def prepare_blocks_menu(blocks_in_file, opts)
1156
- # next if fcb.fetch(:disabled, false)
1157
- # next unless fcb.fetch(:name, '').present?
1158
- blocks_in_file.map do |fcb|
1159
- fcb.merge!(
1160
- name: fcb.dname,
1161
- label: BlockLabel.make(
1162
- body: fcb[:body],
1163
- filename: opts[:filename],
1164
- headings: fcb.fetch(:headings, []),
1165
- menu_blocks_with_docname: opts[:menu_blocks_with_docname],
1166
- menu_blocks_with_headings: opts[:menu_blocks_with_headings],
1167
- text: fcb[:text],
1168
- title: fcb[:title]
1169
- )
1170
- )
1171
- fcb.to_h
1172
- end.compact
1305
+ def saved_name_split(name)
1306
+ # rubocop:disable Layout/LineLength
1307
+ mf = /#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_,_(?<block>.+)\.sh/.match name
1308
+ # rubocop:enable Layout/LineLength
1309
+ return unless mf
1310
+
1311
+ @options[:block_name] = mf[:block]
1312
+ @options[:filename] = mf[:file].gsub(FNR12, FNR11)
1173
1313
  end
1174
1314
 
1175
1315
  # Select and execute a code block from a Markdown document.
@@ -1183,62 +1323,37 @@ module MarkdownExec
1183
1323
  def select_approve_and_execute_block(call_options, &options_block)
1184
1324
  opts = optsmerge(call_options, options_block)
1185
1325
  repeat_menu = true && !opts[:block_name].present?
1186
-
1187
1326
  load_file = !LOAD_FILE
1327
+ default = 1
1328
+
1188
1329
  loop do
1189
- # load file
1190
- #
1191
1330
  loop do
1192
- # repeat menu
1193
- #
1194
- load_file = !LOAD_FILE
1195
- blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
1196
- mdoc = MDoc.new(blocks_in_file) do |nopts|
1197
- opts.merge!(nopts)
1198
- end
1199
- blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
1331
+ opts[:back] = false
1332
+ blocks_in_file, blocks_menu, mdoc = load_file_and_prepare_menu(opts)
1333
+
1200
1334
  unless opts[:block_name].present?
1201
- pt = opts[:prompt_select_block].to_s
1202
- bm = prepare_blocks_menu(blocks_menu, opts)
1203
- return nil if bm.count.zero?
1204
-
1205
- # sel = prompt_with_quit(pt, bm, per_page: opts[:select_page_height])
1206
- # return nil if sel.nil?
1207
- obj = prompt_with_quit2(pt, bm, per_page: opts[:select_page_height])
1208
- case obj.fetch(:option, nil)
1209
- when EXIT_OPTION
1335
+ block_name, state = wait_for_user_selection(blocks_in_file, blocks_menu, default,
1336
+ opts)
1337
+ case state
1338
+ when :exit
1210
1339
  return nil
1211
- when BACK_OPTION
1212
- opts[:filename] = obj[:curr]
1213
- opts[:block_name] = @options[:block_name] = ''
1214
- ENV['MDE_MENU_HISTORY'] = obj[:rest]
1215
- load_file = LOAD_FILE # later: exit menu, load file
1216
- else
1217
- sel = obj[:selected]
1218
-
1219
- ## store selected option
1220
- #
1221
- label_block = blocks_in_file.select do |fcb|
1222
- fcb.dname == sel
1223
- end.fetch(0, nil)
1224
- opts[:block_name] = @options[:block_name] = label_block.oname
1340
+ when :back
1341
+ opts[:block_name] = block_name[:option]
1342
+ opts[:back] = true
1343
+ when :continue
1344
+ opts[:block_name] = block_name
1225
1345
  end
1226
1346
  end
1227
- break if load_file == LOAD_FILE
1228
-
1229
- # later: load file
1230
-
1231
- load_file, block_name = approve_and_execute_block(opts, mdoc)
1232
-
1233
- opts[:block_name] = block_name
1234
- if load_file == LOAD_FILE
1235
- repeat_menu = true
1236
- break
1237
- end
1238
1347
 
1348
+ load_file, next_block_name = approve_and_execute_block(opts, mdoc)
1349
+ default = load_file == LOAD_FILE ? 1 : opts[:block_name]
1350
+ opts[:block_name] = next_block_name
1351
+ break if state == :continue && load_file == LOAD_FILE
1239
1352
  break unless repeat_menu
1240
1353
  end
1241
1354
  break if load_file != LOAD_FILE
1355
+
1356
+ repeat_menu = next_block_name_from_command_line_arguments(opts)
1242
1357
  end
1243
1358
  rescue StandardError => err
1244
1359
  warn(error = "ERROR ** MarkParse.select_approve_and_execute_block(); #{err.inspect}")
@@ -1247,25 +1362,53 @@ module MarkdownExec
1247
1362
  raise ArgumentError, error
1248
1363
  end
1249
1364
 
1250
- def select_md_file(files = list_markdown_files_in_path)
1251
- opts = options
1252
- if (count = files.count) == 1
1253
- files[0]
1254
- elsif count >= 2
1255
- prompt_with_quit opts[:prompt_select_md].to_s, files,
1256
- per_page: opts[:select_page_height]
1365
+ def select_document_if_multiple(files = list_markdown_files_in_path)
1366
+ return files[0] if (count = files.count) == 1
1367
+
1368
+ return unless count >= 2
1369
+
1370
+ opts = options.dup
1371
+ select_option_or_exit opts[:prompt_select_md].to_s, files,
1372
+ opts.merge(per_page: opts[:select_page_height])
1373
+ end
1374
+
1375
+ # Presents a TTY prompt to select an option or exit, returns selected option or nil
1376
+ def select_option_or_exit(prompt_text, items, opts = {})
1377
+ result = select_option_with_metadata(prompt_text, items, opts)
1378
+ return unless result.fetch(:option, nil)
1379
+
1380
+ result[:selected]
1381
+ end
1382
+
1383
+ # Presents a TTY prompt to select an option or exit, returns metadata including option and selected
1384
+ def select_option_with_metadata(prompt_text, items, opts = {})
1385
+ selection = @prompt.select(prompt_text,
1386
+ prompt_menu_add_exit(
1387
+ prompt_menu_add_back(
1388
+ items,
1389
+ opts[:menu_option_back_name]
1390
+ ),
1391
+ opts[:menu_option_exit_name]
1392
+ ),
1393
+ opts.merge(filter: true))
1394
+ if selection == opts[:menu_option_back_name]
1395
+ { option: selection, curr: @hs_curr, rest: @hs_rest, shell: BLOCK_TYPE_LINK }
1396
+ elsif selection == opts[:menu_option_exit_name]
1397
+ { option: selection }
1398
+ else
1399
+ { selected: selection }
1257
1400
  end
1258
1401
  end
1259
1402
 
1260
1403
  def select_recent_output
1261
- filename = prompt_with_quit(
1404
+ filename = select_option_or_exit(
1262
1405
  @options[:prompt_select_output].to_s,
1263
1406
  list_recent_output(
1264
1407
  @options[:saved_stdout_folder],
1265
1408
  @options[:saved_stdout_glob],
1266
1409
  @options[:list_count]
1267
1410
  ),
1268
- { per_page: @options[:select_page_height] }
1411
+ @options.merge({ per_page: @options[:select_page_height] })
1269
1412
  )
1270
1413
  return unless filename.present?
1271
1414
 
@@ -1273,14 +1416,14 @@ module MarkdownExec
1273
1416
  end
1274
1417
 
1275
1418
  def select_recent_script
1276
- filename = prompt_with_quit(
1419
+ filename = select_option_or_exit(
1277
1420
  @options[:prompt_select_md].to_s,
1278
1421
  list_recent_scripts(
1279
1422
  @options[:saved_script_folder],
1280
1423
  @options[:saved_script_glob],
1281
1424
  @options[:list_count]
1282
1425
  ),
1283
- { per_page: @options[:select_page_height] }
1426
+ @options.merge({ per_page: @options[:select_page_height] })
1284
1427
  )
1285
1428
  return if filename.nil?
1286
1429
 
@@ -1293,27 +1436,37 @@ module MarkdownExec
1293
1436
  })
1294
1437
  end
1295
1438
 
1296
- def menu_export(data = menu_for_optparse)
1297
- data.map do |item|
1298
- item.delete(:procname)
1299
- item
1300
- end.to_yaml
1439
+ # set the title of an FCB object based on its body if it is nil or empty
1440
+ def set_fcb_title(fcb)
1441
+ return unless fcb.title.nil? || fcb.title.empty?
1442
+
1443
+ fcb.title = (fcb&.body || []).join(' ').gsub(/ +/, ' ')[0..64]
1301
1444
  end
1302
1445
 
1303
- # Splits the given string on the first occurrence of the specified character.
1304
- # Returns an array containing the portion of the string before the character and the rest of the string.
1305
- #
1306
- # @param input_str [String] The string to be split.
1307
- # @param split_char [String] The character on which to split the string.
1308
- # @return [Array<String>] An array containing two elements: the part of the string before split_char, and the rest of the string.
1309
- def split_string_on_first_char(input_str, split_char)
1310
- split_index = input_str.index(split_char)
1311
-
1312
- if split_index.nil?
1313
- [input_str, '']
1314
- else
1315
- [input_str[0...split_index], input_str[(split_index + 1)..-1]]
1446
+ def start_fenced_block(opts, line, headings, fenced_start_extended_regex)
1447
+ fcb_title_groups = line.match(fenced_start_extended_regex).named_captures.sym_keys
1448
+ rest = fcb_title_groups.fetch(:rest, '')
1449
+
1450
+ fcb = FCB.new
1451
+ fcb.headings = headings
1452
+ fcb.oname = fcb.dname = fcb_title_groups.fetch(:name, '')
1453
+ fcb.shell = fcb_title_groups.fetch(:shell, '')
1454
+ fcb.title = fcb_title_groups.fetch(:name, '')
1455
+ fcb.body = []
1456
+ fcb.reqs, fcb.wraps =
1457
+ ArrayUtil.partition_by_predicate(rest.scan(/\+[^\s]+/).map do |req|
1458
+ req[1..-1]
1459
+ end) do |name|
1460
+ !name.match(Regexp.new(opts[:block_name_wrapper_match]))
1316
1461
  end
1462
+ fcb.call = rest.match(Regexp.new(opts[:block_calls_scan]))&.to_a&.first
1463
+ fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
1464
+ tn.named_captures.sym_keys
1465
+ end
1466
+ fcb.stdout = if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
1467
+ tn.named_captures.sym_keys
1468
+ end
1469
+ fcb
1317
1470
  end
1318
1471
 
1319
1472
  def tab_completions(data = menu_for_optparse)
@@ -1322,6 +1475,77 @@ module MarkdownExec
1322
1475
  end.compact
1323
1476
  end
1324
1477
 
1478
+ def tty_prompt_without_disabled_symbol
1479
+ TTY::Prompt.new(interrupt: :exit, symbols: { cross: ' ' })
1480
+ end
1481
+
1482
+ ##
1483
+ # Updates the hierarchy of document headings based on the given line and existing headings.
1484
+ # The function uses regular expressions specified in the `opts` to identify different levels of headings.
1485
+ #
1486
+ # @param line [String] The line of text to examine for heading content.
1487
+ # @param headings [Array<String>] The existing list of document headings.
1488
+ # @param opts [Hash] A hash containing options for regular expression matches for different heading levels.
1489
+ #
1490
+ # @option opts [String] :heading1_match Regular expression for matching first-level headings.
1491
+ # @option opts [String] :heading2_match Regular expression for matching second-level headings.
1492
+ # @option opts [String] :heading3_match Regular expression for matching third-level headings.
1493
+ #
1494
+ # @return [Array<String>] Updated list of headings.
1495
+ def update_document_headings(line, headings, opts)
1496
+ if (lm = line.match(Regexp.new(opts[:heading3_match])))
1497
+ [headings[0], headings[1], lm[:name]]
1498
+ elsif (lm = line.match(Regexp.new(opts[:heading2_match])))
1499
+ [headings[0], lm[:name]]
1500
+ elsif (lm = line.match(Regexp.new(opts[:heading1_match])))
1501
+ [lm[:name]]
1502
+ else
1503
+ headings
1504
+ end
1505
+ end
1506
+
1507
+ ##
1508
+ # Processes an individual line within a loop, updating headings and handling fenced code blocks.
1509
+ # This function is designed to be called within a loop that iterates through each line of a document.
1510
+ #
1511
+ # @param line [String] The current line being processed.
1512
+ # @param state [Hash] The current state of the parser, including flags and data related to the processing.
1513
+ # @param opts [Hash] A hash containing various options for line and block processing.
1514
+ # @param selected_messages [Array<String>] Accumulator for lines or messages that are subject to further processing.
1515
+ # @param block [Proc] An optional block for further processing or transformation of lines.
1516
+ #
1517
+ # @option state [Array<String>] :headings Current headings to be updated based on the line.
1518
+ # @option state [Regexp] :fenced_start_and_end_regex Regular expression to match the start and end of a fenced block.
1519
+ # @option state [Boolean] :in_fenced_block Flag indicating whether the current line is inside a fenced block.
1520
+ # @option state [Object] :fcb An object representing the current fenced code block being processed.
1521
+ #
1522
+ # @option opts [Boolean] :menu_blocks_with_headings Flag indicating whether to update headings while processing.
1523
+ #
1524
+ # @return [Void] The function modifies the `state` and `selected_messages` arguments in place.
1525
+ ##
1526
+ def update_line_and_block_state(line, state, opts, selected_messages, &block)
1527
+ if opts[:menu_blocks_with_headings]
1528
+ state[:headings] =
1529
+ update_document_headings(line, state[:headings], opts)
1530
+ end
1531
+
1532
+ if line.match(state[:fenced_start_and_end_regex])
1533
+ if state[:in_fenced_block]
1534
+ process_fenced_block(state[:fcb], opts, selected_messages, &block)
1535
+ state[:in_fenced_block] = false
1536
+ else
1537
+ state[:fcb] =
1538
+ start_fenced_block(opts, line, state[:headings],
1539
+ state[:fenced_start_extended_regex])
1540
+ state[:in_fenced_block] = true
1541
+ end
1542
+ elsif state[:in_fenced_block] && state[:fcb].body
1543
+ state[:fcb].body += [line.chomp]
1544
+ else
1545
+ process_line(line, opts, selected_messages, &block)
1546
+ end
1547
+ end
1548
+
1325
1549
  # :reek:BooleanParameter
1326
1550
  # :reek:ControlParameter
1327
1551
  def update_options(opts = {}, over: true)
@@ -1333,6 +1557,29 @@ module MarkdownExec
1333
1557
  @options
1334
1558
  end
1335
1559
 
1560
+ ## Handles the menu interaction and returns selected block name and option state
1561
+ #
1562
+ def wait_for_user_selection(blocks_in_file, blocks_menu, default, opts)
1563
+ pt = opts[:prompt_select_block].to_s
1564
+ bm = prepare_blocks_menu(blocks_menu, opts)
1565
+ return [nil, :exit] if bm.count.zero?
1566
+
1567
+ obj = select_option_with_metadata(pt, bm, opts.merge(
1568
+ default: default,
1569
+ per_page: opts[:select_page_height]
1570
+ ))
1571
+ case obj.fetch(:option, nil)
1572
+ when opts[:menu_option_exit_name]
1573
+ [nil, :exit]
1574
+ when opts[:menu_option_back_name]
1575
+ [obj, :back]
1576
+ else
1577
+ label_block = blocks_in_file.find { |fcb| fcb.dname == obj[:selected] }
1578
+ [label_block.oname, :continue]
1579
+ end
1580
+ end
1581
+
1582
+ # Handles the core logic for generating the command file's metadata and content.
1336
1583
  def write_command_file(call_options, required_lines)
1337
1584
  return unless call_options[:save_executed_script]
1338
1585
 
@@ -1348,22 +1595,51 @@ module MarkdownExec
1348
1595
  @options[:saved_filespec] =
1349
1596
  File.join opts[:saved_script_folder], opts[:saved_script_filename]
1350
1597
 
1351
- dirname = File.dirname(@options[:saved_filespec])
1352
- FileUtils.mkdir_p dirname
1353
1598
  shebang = if @options[:shebang]&.present?
1354
1599
  "#{@options[:shebang]} #{@options[:shell]}\n"
1355
1600
  else
1356
1601
  ''
1357
1602
  end
1358
1603
 
1359
- File.write(@options[:saved_filespec], shebang +
1360
- "# file_name: #{opts[:filename]}\n" \
1361
- "# block_name: #{opts[:block_name]}\n" \
1362
- "# time: #{time_now}\n" \
1363
- "#{required_lines.flatten.join("\n")}\n")
1364
- return if @options[:saved_script_chmod].zero?
1604
+ content = shebang +
1605
+ "# file_name: #{opts[:filename]}\n" \
1606
+ "# block_name: #{opts[:block_name]}\n" \
1607
+ "# time: #{time_now}\n" \
1608
+ "#{required_lines.flatten.join("\n")}\n"
1365
1609
 
1366
- File.chmod @options[:saved_script_chmod], @options[:saved_filespec]
1610
+ create_and_write_file_with_permissions(@options[:saved_filespec], content,
1611
+ @options[:saved_script_chmod])
1612
+ end
1613
+
1614
+ def write_execution_output_to_file
1615
+ FileUtils.mkdir_p File.dirname(@options[:logged_stdout_filespec])
1616
+
1617
+ ol = ["-STDOUT-\n"]
1618
+ ol += @execute_files&.fetch(EF_STDOUT, [])
1619
+ ol += ["\n-STDERR-\n"]
1620
+ ol += @execute_files&.fetch(EF_STDERR, [])
1621
+ ol += ["\n-STDIN-\n"]
1622
+ ol += @execute_files&.fetch(EF_STDIN, [])
1623
+ ol += ["\n"]
1624
+ File.write(@options[:logged_stdout_filespec], ol.join)
1625
+ end
1626
+
1627
+ # Writes required code blocks to a temporary file and sets an environment variable with its path.
1628
+ #
1629
+ # @param block_name [String] The name of the block to collect code for.
1630
+ # @param opts [Hash] Additional options for collecting code.
1631
+ # @note Sets the 'MDE_LINK_REQUIRED_FILE' environment variable to the temporary file path.
1632
+ def write_required_blocks_to_temp_file(mdoc, block_name, opts = {})
1633
+ code_blocks = (read_required_blocks_from_temp_file +
1634
+ mdoc.collect_recursively_required_code(
1635
+ block_name,
1636
+ opts: opts
1637
+ )[:code]).join("\n")
1638
+
1639
+ Dir::Tmpname.create(self.class.to_s) do |path|
1640
+ File.write(path, code_blocks)
1641
+ ENV['MDE_LINK_REQUIRED_FILE'] = path
1642
+ end
1367
1643
  end
1368
1644
  end # class MarkParse
1369
1645
  end # module MarkdownExec
@@ -1388,7 +1664,8 @@ if $PROGRAM_NAME == __FILE__
1388
1664
  c.expects(:command_execute).with(
1389
1665
  obj,
1390
1666
  '',
1391
- args: pigeon)
1667
+ args: pigeon
1668
+ )
1392
1669
 
1393
1670
  # Call method execute_approved_block
1394
1671
  c.execute_approved_block(obj, [])
@@ -1415,7 +1692,8 @@ if $PROGRAM_NAME == __FILE__
1415
1692
  }
1416
1693
  ]
1417
1694
 
1418
- # iterate over the input and output data and assert that the method sets the title as expected
1695
+ # iterate over the input and output data and
1696
+ # assert that the method sets the title as expected
1419
1697
  input_output_data.each do |data|
1420
1698
  input = data[:input]
1421
1699
  output = data[:output]
@@ -1424,13 +1702,5 @@ if $PROGRAM_NAME == __FILE__
1424
1702
  end
1425
1703
  end
1426
1704
  end
1427
-
1428
- ###
1429
-
1430
- # result = split_string_on_first_char("hello-world", "-")
1431
- # puts result.inspect # Output should be ["hello", "world"]
1432
-
1433
- # result = split_string_on_first_char("hello", "-")
1434
- # puts result.inspect # Output should be ["hello", ""]
1435
1705
  end
1436
1706
  end