markdown_exec 1.3.9 → 1.4

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