markdown_exec 1.3.8 → 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,7 +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
+
40
+ # macros
41
+ #
42
+ LOAD_FILE = true
39
43
 
40
44
  # custom error: file specified is missing
41
45
  #
@@ -58,6 +62,18 @@ class Hash
58
62
  end
59
63
  end
60
64
 
65
+ # integer value for comparison
66
+ #
67
+ def options_fetch_display_level(options)
68
+ options.fetch(:display_level, 1)
69
+ end
70
+
71
+ # integer value for comparison
72
+ #
73
+ def options_fetch_display_level_xbase_prefix(options)
74
+ options.fetch(:level_xbase_prefix, '')
75
+ end
76
+
61
77
  # stdout manager
62
78
  #
63
79
  module FOUT
@@ -77,7 +93,7 @@ module FOUT
77
93
  end
78
94
 
79
95
  def approved_fout?(level)
80
- level <= @options[:display_level]
96
+ level <= options_fetch_display_level(@options)
81
97
  end
82
98
 
83
99
  # display output at level or lower than filter (DISPLAY_LEVEL_DEFAULT)
@@ -85,7 +101,7 @@ module FOUT
85
101
  def lout(str, level: DISPLAY_LEVEL_BASE)
86
102
  return unless approved_fout? level
87
103
 
88
- fout level == DISPLAY_LEVEL_BASE ? str : @options[:display_level_xbase_prefix] + str
104
+ fout level == DISPLAY_LEVEL_BASE ? str : options_fetch_display_level_xbase_prefix(@options) + str
89
105
  end
90
106
  end
91
107
 
@@ -110,10 +126,45 @@ end
110
126
  # convert regex match groups to a hash with symbol keys
111
127
  #
112
128
  # :reek:UtilityFunction
113
- def option_match_groups(str, option)
129
+ def extract_named_captures_from_option(str, option)
114
130
  str.match(Regexp.new(option))&.named_captures&.sym_keys
115
131
  end
116
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
+
117
168
  # execute markdown documents
118
169
  #
119
170
  module MarkdownExec
@@ -121,6 +172,13 @@ module MarkdownExec
121
172
  FNR11 = '/'
122
173
  FNR12 = ',~'
123
174
 
175
+ SHELL_COLOR_OPTIONS = {
176
+ BLOCK_TYPE_BASH => :menu_bash_color,
177
+ BLOCK_TYPE_LINK => :menu_link_color,
178
+ BLOCK_TYPE_OPTS => :menu_opts_color,
179
+ BLOCK_TYPE_VARS => :menu_vars_color
180
+ }.freeze
181
+
124
182
  ##
125
183
  #
126
184
  # rubocop:disable Layout/LineLength
@@ -132,12 +190,11 @@ module MarkdownExec
132
190
  class MarkParse
133
191
  attr_reader :options
134
192
 
193
+ include ArrayUtil
194
+ include StringUtil
135
195
  include FOUT
136
196
 
137
197
  def initialize(options = {})
138
- @options = options
139
- # hide disabled symbol
140
- @prompt = TTY::Prompt.new(interrupt: :exit, symbols: { cross: ' ' })
141
198
  @execute_aborted_at = nil
142
199
  @execute_completed_at = nil
143
200
  @execute_error = nil
@@ -147,6 +204,74 @@ module MarkdownExec
147
204
  @execute_script_filespec = nil
148
205
  @execute_started_at = nil
149
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
150
275
  end
151
276
 
152
277
  # return arguments before `--`
@@ -162,17 +287,6 @@ module MarkdownExec
162
287
  end
163
288
  end
164
289
 
165
- # return arguments after `--`
166
- #
167
- def arguments_for_child(argv = ARGV)
168
- case ind = argv.find_index('--')
169
- when nil, argv.count - 1
170
- []
171
- else
172
- argv[ind + 1..-1]
173
- end
174
- end
175
-
176
290
  ##
177
291
  # options necessary to start, parse input, defaults for cli options
178
292
  #
@@ -191,6 +305,14 @@ module MarkdownExec
191
305
  end.compact.to_h
192
306
  end
193
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
+
194
316
  def calculated_options
195
317
  {
196
318
  bash: true, # bash block parsing in get_block_summary()
@@ -199,133 +321,84 @@ module MarkdownExec
199
321
  }
200
322
  end
201
323
 
202
- # Execute a code block after approval and provide user interaction options.
203
- #
204
- # This method displays required code blocks, asks for user approval, and
205
- # executes the code block if approved. It also allows users to copy the
206
- # code to the clipboard or save it to a file.
207
- #
208
- # @param opts [Hash] Options hash containing configuration settings.
209
- # @param mdoc [YourMDocClass] An instance of the MDoc class.
210
- # @return [String] The name of the executed code block.
211
- def approve_and_execute_block(opts, mdoc)
212
- # Collect required code blocks based on the provided options.
213
- required_blocks = mdoc.collect_recursively_required_code(opts[:block_name])
214
- # Display required code blocks if requested or required approval.
215
- if opts[:output_script] || opts[:user_must_approve]
216
- display_required_code(opts,
217
- required_blocks)
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
218
329
  end
219
330
 
220
- allow = true
221
- # If user approval is required, prompt the user for approval.
222
- if opts[:user_must_approve]
223
- loop do
224
- # Present a selection menu for user approval.
225
- sel = @prompt.select(opts[:prompt_approve_block],
226
- filter: true) do |menu|
227
- menu.default 1
228
- menu.choice opts[:prompt_yes], 1
229
- menu.choice opts[:prompt_no], 2
230
- menu.choice opts[:prompt_script_to_clipboard], 3
231
- menu.choice opts[:prompt_save_script], 4
232
- end
233
- allow = (sel == 1)
234
- if sel == 3
235
- # Copy the code to the clipboard.
236
- text = required_blocks.flatten.join($INPUT_RECORD_SEPARATOR)
237
- Clipboard.copy(text)
238
- fout "Clipboard updated: #{required_blocks.count} blocks," /
239
- " #{required_blocks.flatten.count} lines," /
240
- " #{text.length} characters"
241
- end
242
- if sel == 4
243
- # Save the code to a file.
244
- write_command_file(opts.merge(save_executed_script: true),
245
- required_blocks)
246
- fout "File saved: #{@options[:saved_filespec]}"
247
- end
248
- break if [1, 2].include? sel
249
- end
331
+ unless File.exist? filename
332
+ fout 'Document is missing.'
333
+ return false
250
334
  end
335
+ true
336
+ end
251
337
 
252
- opts[:ir_approve] = allow
253
-
254
- # Get the selected code block by name.
255
- selected = mdoc.get_block_by_name(opts[:block_name])
338
+ def clear_required_file
339
+ ENV['MDE_LINK_REQUIRED_FILE'] = ''
340
+ end
256
341
 
257
- # If approved, write the code to a file, execute it, and provide output.
258
- if opts[:ir_approve]
259
- write_command_file(opts, required_blocks)
260
- command_execute(opts, required_blocks.flatten.join("\n"))
261
- save_execution_output
262
- output_execution_summary
263
- output_execution_result
342
+ # Collect required code blocks based on the provided options.
343
+ #
344
+ # @param opts [Hash] Options hash containing configuration settings.
345
+ # @param mdoc [YourMDocClass] An instance of the MDoc class.
346
+ # @return [Array<String>] Required code blocks as an array of lines.
347
+ def collect_required_code_lines(mdoc, selected, opts: {})
348
+ # Apply hash in opts block to environment variables
349
+ if selected[:shell] == BLOCK_TYPE_VARS
350
+ data = YAML.load(selected[:body].join("\n"))
351
+ data.each_key do |key|
352
+ ENV[key] = value = data[key].to_s
353
+ next unless opts[:menu_vars_set_format].present?
354
+
355
+ print format(
356
+ opts[:menu_vars_set_format],
357
+ { key: key,
358
+ value: value }
359
+ ).send(opts[:menu_vars_set_color].to_sym)
360
+ end
264
361
  end
265
362
 
266
- selected[:name]
363
+ required = mdoc.collect_recursively_required_code(opts[:block_name], opts: opts)
364
+ read_required_blocks_from_temp_file + required[:code]
267
365
  end
268
366
 
269
367
  def cfile
270
- @cfile ||= CachedNestedFileReader.new(import_pattern: @options.fetch(:import_pattern))
368
+ @cfile ||= CachedNestedFileReader.new(
369
+ import_pattern: @options.fetch(:import_pattern)
370
+ )
271
371
  end
272
372
 
273
- # :reek:DuplicateMethodCall
274
- # :reek:UncommunicativeVariableName { exclude: [ e ] }
275
- # :reek:LongYieldList
276
- def command_execute(opts, command)
277
- #d 'execute command and yield outputs'
373
+ EF_STDOUT = :stdout
374
+ EF_STDERR = :stderr
375
+ EF_STDIN = :stdin
376
+
377
+ # Existing command_execute method
378
+ def command_execute(opts, command, args: [])
278
379
  @execute_files = Hash.new([])
279
380
  @execute_options = opts
280
381
  @execute_started_at = Time.now.utc
281
382
 
282
- args = []
283
- Open3.popen3(@options[:shell], '-c',
284
- command, ARGV[0], *args) do |stdin, stdout, stderr, exec_thr|
285
- #d 'command started'
286
- Thread.new do
287
- until (line = stdout.gets).nil?
288
- @execute_files[EF_STDOUT] = @execute_files[EF_STDOUT] + [line]
289
- print line if opts[:output_stdout]
290
- yield nil, line, nil, exec_thr if block_given?
291
- end
292
- rescue IOError
293
- #d 'stdout IOError, thread killed, do nothing'
383
+ Open3.popen3(opts[:shell], '-c', command, opts[:filename],
384
+ *args) do |stdin, stdout, stderr, exec_thr|
385
+ handle_stream(opts, stdout, EF_STDOUT) do |line|
386
+ yield nil, line, nil, exec_thr if block_given?
294
387
  end
295
-
296
- Thread.new do
297
- until (line = stderr.gets).nil?
298
- @execute_files[EF_STDERR] = @execute_files[EF_STDERR] + [line]
299
- print line if opts[:output_stdout]
300
- yield nil, nil, line, exec_thr if block_given?
301
- end
302
- rescue IOError
303
- #d 'stderr IOError, thread killed, do nothing'
388
+ handle_stream(opts, stderr, EF_STDERR) do |line|
389
+ yield nil, nil, line, exec_thr if block_given?
304
390
  end
305
391
 
306
- in_thr = Thread.new do
307
- while exec_thr.alive? # reading input until the child process ends
308
- stdin.puts(line = $stdin.gets)
309
- @execute_files[EF_STDIN] = @execute_files[EF_STDIN] + [line]
310
- yield line, nil, nil, exec_thr if block_given?
311
- end
312
- #d 'exec_thr now dead'
313
- rescue StandardError
314
- #d 'stdin error, thread killed, do nothing'
392
+ in_thr = handle_stream(opts, $stdin, EF_STDIN) do |line|
393
+ stdin.puts(line)
394
+ yield line, nil, nil, exec_thr if block_given?
315
395
  end
316
396
 
317
- #d 'join exec_thr'
318
397
  exec_thr.join
319
-
320
- #d 'wait before closing stdin'
321
398
  sleep 0.1
322
-
323
- #d 'kill stdin thread'
324
- in_thr.kill
325
- # @return_code = exec_thr.value
326
- #d 'command end'
399
+ in_thr.kill if in_thr&.alive?
327
400
  end
328
- #d 'command completed'
401
+
329
402
  @execute_completed_at = Time.now.utc
330
403
  rescue Errno::ENOENT => err
331
404
  #d 'command error ENOENT triggered by missing command in script'
@@ -343,31 +416,104 @@ module MarkdownExec
343
416
  fout "Error ENOENT: #{err.inspect}"
344
417
  end
345
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
+
346
427
  def count_blocks_in_filename
347
- fenced_start_and_end_match = Regexp.new @options[:fenced_start_and_end_match]
428
+ fenced_start_and_end_regex = Regexp.new @options[:fenced_start_and_end_regex]
348
429
  cnt = 0
349
430
  cfile.readlines(@options[:filename]).each do |line|
350
- cnt += 1 if line.match(fenced_start_and_end_match)
431
+ cnt += 1 if line.match(fenced_start_and_end_regex)
351
432
  end
352
433
  cnt / 2
353
434
  end
354
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
+
355
475
  # :reek:DuplicateMethodCall
356
- def display_required_code(opts, required_blocks)
476
+ def display_required_code(opts, required_lines)
357
477
  frame = opts[:output_divider].send(opts[:output_divider_color].to_sym)
358
478
  fout frame
359
- required_blocks.each { |cb| fout cb }
479
+ required_lines.each { |cb| fout cb }
360
480
  fout frame
361
481
  end
362
482
 
363
- # :reek:DuplicateMethodCall
364
- def exec_block(options, _block_name = '')
365
- options = calculated_options.merge(options)
366
- 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
367
494
 
368
- # document and block reports
369
- #
370
- 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)
371
517
 
372
518
  simple_commands = {
373
519
  doc_glob: -> { fout options[:md_filename_glob] },
@@ -398,27 +544,59 @@ module MarkdownExec
398
544
  tab_completions: -> { fout tab_completions },
399
545
  menu_export: -> { fout menu_export }
400
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)
401
563
  simple_commands.each_key do |key|
402
564
  if @options[key]
403
565
  simple_commands[key].call
404
- return # rubocop:disable Lint/NonLocalExitFromIterator
566
+ return true
405
567
  end
406
568
  end
569
+ false
570
+ end
407
571
 
408
- # process
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
409
577
  #
410
- @options[:filename] = select_md_file(files)
411
- select_approve_and_execute_block({
412
- bash: true,
413
- struct: true
414
- })
415
- return unless @options[:output_saved_script_filename]
578
+ %i[blocks line]
579
+ end
416
580
 
417
- fout "saved_filespec: #{@execute_script_filespec}"
418
- rescue StandardError => err
419
- warn(error = "ERROR ** MarkParse.exec_block(); #{err.inspect}")
420
- binding.pry if $tap_enable
421
- raise ArgumentError, error
581
+ ## post-parse options configuration
582
+ #
583
+ def finalize_cli_argument_processing(rest)
584
+ ## position 0: file or folder (optional)
585
+ #
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
595
+
596
+ ## position 1: block name (optional)
597
+ #
598
+ block_name = rest.shift
599
+ @options[:block_name] = block_name if block_name.present?
422
600
  end
423
601
 
424
602
  ## summarize blocks
@@ -426,7 +604,6 @@ module MarkdownExec
426
604
  def get_block_summary(call_options, fcb)
427
605
  opts = optsmerge call_options
428
606
  # return fcb.body unless opts[:struct]
429
-
430
607
  return fcb unless opts[:bash]
431
608
 
432
609
  fcb.call = fcb.title.match(Regexp.new(opts[:block_calls_scan]))&.fetch(1, nil)
@@ -435,207 +612,282 @@ module MarkdownExec
435
612
  else
436
613
  fcb.title
437
614
  end
438
- bm = option_match_groups(titlexcall, opts[:block_name_match])
439
- fcb.stdin = option_match_groups(titlexcall, opts[:block_stdin_scan])
440
- fcb.stdout = option_match_groups(titlexcall, opts[:block_stdout_scan])
441
- fcb.title = fcb.name = (bm && bm[1] ? bm[:title] : titlexcall)
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])
618
+
619
+ shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
620
+ fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall
621
+ fcb.dname = if shell_color_option && opts[shell_color_option].present?
622
+ fcb.oname.send(opts[shell_color_option].to_sym)
623
+ else
624
+ fcb.oname
625
+ end
442
626
  fcb
443
627
  end
444
628
 
445
- # :reek:DuplicateMethodCall
446
- # :reek:LongYieldList
447
- # :reek:NestedIterators
448
- def iter_blocks_in_file(opts = {})
449
- # opts = optsmerge call_options, options_block
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
450
637
 
451
- unless opts[:filename]&.present?
452
- fout 'No blocks found.'
453
- return
454
- 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
455
646
 
456
- unless File.exist? opts[:filename]
457
- fout 'Document is missing.'
458
- 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)
459
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]
460
662
 
461
- fenced_start_and_end_match = Regexp.new opts[:fenced_start_and_end_match]
462
- fenced_start_ex = Regexp.new opts[:fenced_start_ex_match]
463
- fcb = FCB.new
464
- in_block = false
465
- headings = []
663
+ [!LOAD_FILE, '']
664
+ end
466
665
 
467
- ## get type of messages to select
468
- #
469
- 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
676
+
677
+ history_state_push(mdoc, data_file, opts)
678
+
679
+ data.fetch('vars', []).each do |var|
680
+ ENV[var[0]] = var[1].to_s
681
+ end
470
682
 
471
- cfile.readlines(opts[:filename]).each.with_index do |line, _line_num|
472
- continue unless line
683
+ [LOAD_FILE, data.fetch('block', '')]
684
+ end
473
685
 
474
- if opts[:menu_blocks_with_headings]
475
- if (lm = line.match(Regexp.new(opts[:heading3_match])))
476
- headings = [headings[0], headings[1], lm[:name]]
477
- elsif (lm = line.match(Regexp.new(opts[:heading2_match])))
478
- headings = [headings[0], lm[:name]]
479
- elsif (lm = line.match(Regexp.new(opts[:heading1_match])))
480
- headings = [lm[:name]]
481
- end
482
- end
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?
696
+
697
+ print format(
698
+ opts[:menu_opts_set_format],
699
+ { key: key,
700
+ value: value }
701
+ ).send(opts[:menu_opts_set_color].to_sym)
702
+ end
703
+ [!LOAD_FILE, '']
704
+ end
483
705
 
484
- if line.match(fenced_start_and_end_match)
485
- if in_block
486
- # end fcb
487
- #
488
- fcb.name = fcb.title || ''
489
- if fcb.body
490
- if fcb.title.nil? || fcb.title.empty?
491
- fcb.title = fcb.body.join(' ').gsub(/ +/, ' ')[0..64]
492
- end
493
-
494
- if block_given? &&
495
- selected_messages.include?(:blocks) &&
496
- Filter.fcb_select?(opts, fcb)
497
- yield :blocks, fcb
498
- end
499
- end
500
- in_block = false
501
- else
502
- # start fcb
503
- #
504
- in_block = true
505
-
506
- fcb_title_groups = line.match(fenced_start_ex).named_captures.sym_keys
507
- fcb = FCB.new
508
- fcb.headings = headings
509
- fcb.name = fcb_title_groups.fetch(:name, '')
510
- fcb.shell = fcb_title_groups.fetch(:shell, '')
511
- fcb.title = fcb_title_groups.fetch(:name, '')
512
-
513
- # selected fcb
514
- #
515
- fcb.body = []
516
-
517
- rest = fcb_title_groups.fetch(:rest, '')
518
- fcb.reqs, fcb.wraps =
519
- split_array(rest.scan(/\+[^\s]+/).map { |req| req[1..-1] }) do |name|
520
- !name.match(Regexp.new(opts[:block_name_wrapper_match]))
521
- end
522
- fcb.call = rest.match(Regexp.new(opts[:block_calls_scan]))&.to_a&.first
523
- fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
524
- tn.named_captures.sym_keys
525
- end
526
- fcb.stdout = if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
527
- tn.named_captures.sym_keys
528
- end
529
- end
530
- elsif in_block && fcb.body
531
- dp 'append line to fcb body'
532
- fcb.body += [line.chomp]
533
- elsif block_given? && selected_messages.include?(:line)
534
- dp 'text outside of fcb'
535
- fcb = FCB.new
536
- fcb.body = [line]
537
- yield :line, 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?
538
716
  end
717
+ rescue IOError
718
+ #d 'stdout IOError, thread killed, do nothing'
539
719
  end
540
720
  end
541
721
 
542
- def split_array(arr)
543
- true_list = []
544
- false_list = []
722
+ def history_state_exist?
723
+ history = ENV.fetch(MDE_HISTORY_ENV_NAME, '')
724
+ history.present? ? history : nil
725
+ end
545
726
 
546
- arr.each do |element|
547
- if yield(element)
548
- true_list << element
549
- else
550
- false_list << element
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
733
+ end
734
+
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
741
+
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
750
+ end
751
+
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")
757
+
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
551
768
  end
552
769
  end
770
+ @option_parser.load
771
+ @option_parser.environment
772
+
773
+ rest = @option_parser.parse!(arguments_for_mde)
774
+ @options[:pass_args] = ARGV[rest.count + 1..]
775
+
776
+ rest
777
+ end
778
+
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
553
795
 
554
- [true_list, false_list]
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
+ }
555
805
  end
556
806
 
557
- # # Example usage:
558
- # array = [1, 2, 3, 4, 5]
559
- # result = split_array(array) { |num| num.even? }
560
- # puts "True List: #{result[0]}" # Output: True List: [2, 4]
561
- # puts "False List: #{result[1]}" # Output: False List: [1, 3, 5]
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
562
818
 
563
- # return body, title if option.struct
564
- # return body if not struct
819
+ update_line_and_block_state(line, state, opts, selected_messages, &block)
820
+ end
821
+ end
822
+
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.
565
830
  #
566
831
  def list_blocks_in_file(call_options = {}, &options_block)
567
832
  opts = optsmerge(call_options, options_block)
568
833
  use_chrome = !opts[:no_chrome]
569
834
 
570
835
  blocks = []
571
- if opts[:menu_initial_divider].present? && use_chrome
572
- blocks.push FCB.new({
573
- # name: '',
574
- chrome: true,
575
- name: format(
576
- opts[:menu_divider_format],
577
- opts[:menu_initial_divider]
578
- ).send(opts[:menu_divider_color].to_sym),
579
- disabled: '' # __LINE__.to_s
580
- })
581
- end
836
+ append_initial_divider(blocks, opts) if use_chrome
582
837
 
583
838
  iter_blocks_in_file(opts) do |btype, fcb|
584
839
  case btype
585
840
  when :filter
586
- ## return type of blocks to select
587
- #
588
- %i[blocks line]
589
-
841
+ filter_block_types
590
842
  when :line
591
- ## convert line to block
592
- #
593
- if opts[:menu_divider_match].present? &&
594
- (mbody = fcb.body[0].match opts[:menu_divider_match])
595
- if use_chrome
596
- blocks.push FCB.new(
597
- { chrome: true,
598
- disabled: '',
599
- name: format(opts[:menu_divider_format],
600
- mbody[:name]).send(opts[:menu_divider_color].to_sym) }
601
- )
602
- end
603
- elsif opts[:menu_task_match].present? &&
604
- (fcb.body[0].match opts[:menu_task_match])
605
- if use_chrome
606
- blocks.push FCB.new(
607
- { chrome: true,
608
- disabled: '',
609
- name: format(
610
- opts[:menu_task_format],
611
- $~.named_captures.transform_keys(&:to_sym)
612
- ).send(opts[:menu_task_color].to_sym) }
613
- )
614
- end
615
- else
616
- # line not added
617
- end
843
+ process_line_blocks(blocks, fcb, opts, use_chrome)
618
844
  when :blocks
619
- ## enhance fcb with block summary
620
- #
621
- blocks.push get_block_summary(opts, fcb) ### if Filter.fcb_select? opts, fcb
845
+ append_block_summary(blocks, fcb, opts)
622
846
  end
623
847
  end
624
848
 
625
- if opts[:menu_divider_format].present? && opts[:menu_final_divider].present? && use_chrome && use_chrome
626
- blocks.push FCB.new(
627
- { chrome: true,
628
- disabled: '',
629
- name: format(opts[:menu_divider_format],
630
- opts[:menu_final_divider])
631
- .send(opts[:menu_divider_color].to_sym) }
632
- )
633
- end
849
+ append_final_divider(blocks, opts) if use_chrome
634
850
  blocks
635
851
  rescue StandardError => err
636
- warn(error = "ERROR ** MarkParse.list_blocks_in_file(); #{err.inspect}")
637
- warn(caller[0..4])
638
- 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
639
891
  end
640
892
 
641
893
  def list_default_env
@@ -662,39 +914,22 @@ module MarkdownExec
662
914
 
663
915
  def list_files_per_options(options)
664
916
  list_files_specified(
665
- specified_filename: options[:filename]&.present? ? options[:filename] : nil,
666
- specified_folder: options[:path],
667
- default_filename: 'README.md',
668
- 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
+ )
669
923
  )
670
924
  end
671
925
 
672
- # :reek:LongParameterList
673
- def list_files_specified(specified_filename: nil, specified_folder: nil,
674
- default_filename: nil, default_folder: nil, filetree: nil)
675
- fn = File.join(if specified_filename&.present?
676
- if specified_filename.start_with? '/'
677
- [specified_filename]
678
- elsif specified_folder&.present?
679
- [specified_folder, specified_filename]
680
- else
681
- [default_folder, specified_filename]
682
- end
683
- elsif specified_folder&.present?
684
- if filetree
685
- [specified_folder, @options[:md_filename_match]]
686
- else
687
- [specified_folder, @options[:md_filename_glob]]
688
- end
689
- else
690
- [default_folder, default_filename]
691
- end)
692
- if filetree
693
- filetree.select do |filename|
694
- filename == fn || filename.match(/^#{fn}$/) || filename.match(%r{^#{fn}/.+$})
695
- end
696
- else
697
- 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}/.+$})
698
933
  end
699
934
  end
700
935
 
@@ -703,17 +938,6 @@ module MarkdownExec
703
938
  @options[:md_filename_glob]))
704
939
  end
705
940
 
706
- def blocks_per_opts(blocks, opts)
707
- if opts[:struct]
708
- blocks
709
- else
710
- # blocks.map(&:name)
711
- blocks.map do |block|
712
- block.fetch(:text, nil) || block.fetch(:name, nil)
713
- end
714
- end.compact.reject(&:empty?)
715
- end
716
-
717
941
  ## output type (body string or full object) per option struct and bash
718
942
  #
719
943
  def list_named_blocks_in_file(call_options = {}, &options_block)
@@ -726,6 +950,17 @@ module MarkdownExec
726
950
  blocks_per_opts(blocks, opts)
727
951
  end
728
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
+
729
964
  def make_block_labels(call_options = {})
730
965
  opts = options.merge(call_options)
731
966
  list_blocks_in_file(opts).map do |fcb|
@@ -741,6 +976,32 @@ module MarkdownExec
741
976
  end.compact
742
977
  end
743
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
+
744
1005
  # :reek:DuplicateMethodCall
745
1006
  # :reek:NestedIterators
746
1007
  def menu_for_optparse
@@ -764,11 +1025,11 @@ module MarkdownExec
764
1025
  }
765
1026
  when 'path'
766
1027
  lambda { |value|
767
- read_configuration_file! options, value
1028
+ read_configuration_file!(options, value)
768
1029
  }
769
1030
  when 'show_config'
770
1031
  lambda { |_|
771
- options_finalize options
1032
+ finalize_cli_argument_processing(options)
772
1033
  fout options.sort_by_key.to_yaml
773
1034
  }
774
1035
  when 'val_as_bool'
@@ -792,33 +1053,14 @@ module MarkdownExec
792
1053
  end
793
1054
  end
794
1055
 
795
- def menu_for_blocks(menu_options)
796
- options = calculated_options.merge menu_options
797
- menu = []
798
- iter_blocks_in_file(options) do |btype, fcb|
799
- case btype
800
- when :filter
801
- %i[blocks line]
802
- when :line
803
- if options[:menu_divider_match] &&
804
- (mbody = fcb.body[0].match(options[:menu_divider_match]))
805
- menu.push FCB.new({ name: mbody[:name], disabled: '' })
806
- end
807
- when :blocks
808
- menu += [fcb.name]
809
- end
810
- end
811
- menu
1056
+ def menu_help
1057
+ @option_parser.help
812
1058
  end
813
1059
 
814
1060
  def menu_iter(data = menu_for_optparse, &block)
815
1061
  data.map(&block)
816
1062
  end
817
1063
 
818
- def menu_help
819
- @option_parser.help
820
- end
821
-
822
1064
  def menu_option_append(opts, options, item)
823
1065
  return unless item[:long_name].present? || item[:short_name].present?
824
1066
 
@@ -846,27 +1088,6 @@ module MarkdownExec
846
1088
  ].compact)
847
1089
  end
848
1090
 
849
- ## post-parse options configuration
850
- #
851
- def options_finalize(rest)
852
- ## position 0: file or folder (optional)
853
- #
854
- if (pos = rest.fetch(0, nil))&.present?
855
- if Dir.exist?(pos)
856
- @options[:path] = pos
857
- elsif File.exist?(pos)
858
- @options[:filename] = pos
859
- else
860
- raise FileMissingError, pos, caller
861
- end
862
- end
863
-
864
- ## position 1: block name (optional)
865
- #
866
- block_name = rest.fetch(1, nil)
867
- @options[:block_name] = block_name if block_name.present?
868
- end
869
-
870
1091
  # :reek:ControlParameter
871
1092
  def optsmerge(call_options = {}, options_block = nil)
872
1093
  class_call_options = @options.merge(call_options || {})
@@ -910,25 +1131,113 @@ module MarkdownExec
910
1131
  }
911
1132
  end
912
1133
 
913
- ## insert exit option at head or tail
1134
+ # Prepare the blocks menu by adding labels and other necessary details.
914
1135
  #
915
- def prompt_menu_add_exit(_prompt_text, items, exit_option, _opts = {})
916
- if @options[:menu_exit_at_top]
917
- (@options[:menu_with_exit] ? [exit_option] : []) + items
918
- else
919
- items + (@options[:menu_with_exit] ? [exit_option] : [])
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
1158
+
1159
+ # Prepares and fetches file listings
1160
+ def prepare_file_list(options)
1161
+ list_files_per_options(options)
1162
+ end
1163
+
1164
+ def process_fenced_block(fcb, opts, selected_messages, &block)
1165
+ fcb.oname = fcb.dname = fcb.title || ''
1166
+ return unless fcb.body
1167
+
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
1175
+ end
1176
+
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.
1189
+ #
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
920
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
1218
+ end
1219
+
1220
+ ## insert back option at head or tail
1221
+ #
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]
921
1231
  end
922
1232
 
923
- ## tty prompt to select
924
- # insert exit option at head or tail
925
- # return selected option or nil
1233
+ ## insert exit option at head or tail
926
1234
  #
927
- def prompt_with_quit(prompt_text, items, opts = {})
928
- exit_option = '* Exit'
929
- sel = @prompt.select(prompt_text, prompt_menu_add_exit(prompt_text, items, exit_option, opts),
930
- opts.merge(filter: true))
931
- sel == exit_option ? nil : sel
1235
+ def prompt_menu_add_exit(items, label)
1236
+ if @options[:menu_exit_at_top]
1237
+ (@options[:menu_with_exit] ? [label] : []) + items
1238
+ else
1239
+ items + (@options[:menu_with_exit] ? [label] : [])
1240
+ end
932
1241
  end
933
1242
 
934
1243
  # :reek:UtilityFunction ### temp
@@ -939,57 +1248,33 @@ module MarkdownExec
939
1248
  .transform_keys(&:to_sym))
940
1249
  end
941
1250
 
942
- # :reek:NestedIterators
943
- def run
944
- ## default configuration
945
- #
946
- @options = base_options
947
-
948
- ## read local configuration file
949
- #
950
- read_configuration_file! @options,
951
- ".#{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 = []
952
1257
 
953
- @option_parser = option_parser = OptionParser.new do |opts|
954
- executable_name = File.basename($PROGRAM_NAME)
955
- opts.banner = [
956
- "#{MarkdownExec::APP_NAME}" \
957
- " - #{MarkdownExec::APP_DESC} (#{MarkdownExec::VERSION})",
958
- "Usage: #{executable_name} [(path | filename [block_name])] [options]"
959
- ].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?
960
1260
 
961
- menu_iter do |item|
962
- menu_option_append opts, options, item
963
- end
964
- end
965
- option_parser.load # filename defaults to basename of the program
966
- # without suffix in a directory ~/.options
967
- option_parser.environment # env defaults to the basename of the program
968
- # child_argv = arguments_for_child
969
- rest = option_parser.parse!(arguments_for_mde) # (into: options)
970
-
971
- begin
972
- options_finalize rest
973
- exec_block options, options[:block_name]
974
- rescue FileMissingError => err
975
- puts "File missing: #{err}"
1261
+ if File.exist?(temp_blocks_file_path)
1262
+ temp_blocks = File.readlines(temp_blocks_file_path, chomp: true)
976
1263
  end
1264
+
1265
+ temp_blocks
1266
+ end
1267
+
1268
+ def run
1269
+ clear_required_file
1270
+ execute_block_with_error_handling(initialize_and_parse_cli_options)
1271
+ delete_required_temp_file
977
1272
  rescue StandardError => err
978
1273
  warn(error = "ERROR ** MarkParse.run(); #{err.inspect}")
979
1274
  binding.pry if $tap_enable
980
1275
  raise ArgumentError, error
981
1276
  end
982
1277
 
983
- def saved_name_split(name)
984
- # rubocop:disable Layout/LineLength
985
- mf = /#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_,_(?<block>.+)\.sh/.match name
986
- # rubocop:enable Layout/LineLength
987
- return unless mf
988
-
989
- @options[:block_name] = mf[:block]
990
- @options[:filename] = mf[:file].gsub(FNR12, FNR11)
991
- end
992
-
993
1278
  def run_last_script
994
1279
  filename = SavedFilesMatcher.most_recent(@options[:saved_script_folder],
995
1280
  @options[:saved_script_glob])
@@ -1000,30 +1285,20 @@ module MarkdownExec
1000
1285
  select_approve_and_execute_block({})
1001
1286
  end
1002
1287
 
1003
- def save_execution_output
1004
- return unless @options[:save_execution_output]
1005
-
1006
- @options[:logged_stdout_filename] =
1007
- SavedAsset.stdout_name(blockname: @options[:block_name],
1008
- filename: File.basename(@options[:filename], '.*'),
1009
- prefix: @options[:logged_stdout_filename_prefix],
1010
- time: Time.now.utc)
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]}"
1292
+ end
1011
1293
 
1012
- @options[:logged_stdout_filespec] =
1013
- File.join @options[:saved_stdout_folder],
1014
- @options[:logged_stdout_filename]
1015
- @logged_stdout_filespec = @options[:logged_stdout_filespec]
1016
- (dirname = File.dirname(@options[:logged_stdout_filespec]))
1017
- FileUtils.mkdir_p dirname
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
1018
1299
 
1019
- ol = ["-STDOUT-\n"]
1020
- ol += @execute_files&.fetch(EF_STDOUT, [])
1021
- ol += ["\n-STDERR-\n"]
1022
- ol += @execute_files&.fetch(EF_STDERR, [])
1023
- ol += ["\n-STDIN-\n"]
1024
- ol += @execute_files&.fetch(EF_STDIN, [])
1025
- ol += ["\n"]
1026
- File.write(@options[:logged_stdout_filespec], ol.join)
1300
+ @options[:block_name] = mf[:block]
1301
+ @options[:filename] = mf[:file].gsub(FNR12, FNR11)
1027
1302
  end
1028
1303
 
1029
1304
  # Select and execute a code block from a Markdown document.
@@ -1036,51 +1311,37 @@ module MarkdownExec
1036
1311
  # @return [Nil] Returns nil if no code block is selected or an error occurs.
1037
1312
  def select_approve_and_execute_block(call_options, &options_block)
1038
1313
  opts = optsmerge(call_options, options_block)
1039
- blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
1040
- mdoc = MDoc.new(blocks_in_file) do |nopts|
1041
- opts.merge!(nopts)
1042
- end
1043
- blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
1044
-
1045
1314
  repeat_menu = true && !opts[:block_name].present?
1315
+ load_file = !LOAD_FILE
1316
+ default = 1
1317
+
1046
1318
  loop do
1047
- unless opts[:block_name].present?
1048
- pt = opts[:prompt_select_block].to_s
1049
-
1050
- bm = blocks_menu.map do |fcb|
1051
- # next if fcb.fetch(:disabled, false)
1052
- # next unless fcb.fetch(:name, '').present?
1053
-
1054
- fcb.merge!(
1055
- label: BlockLabel.make(
1056
- body: fcb[:body],
1057
- filename: opts[:filename],
1058
- headings: fcb.fetch(:headings, []),
1059
- menu_blocks_with_docname: opts[:menu_blocks_with_docname],
1060
- menu_blocks_with_headings: opts[:menu_blocks_with_headings],
1061
- text: fcb[:text],
1062
- title: fcb[:title]
1063
- )
1064
- )
1065
-
1066
- fcb.to_h
1067
- end.compact
1068
- return nil if bm.count.zero?
1069
-
1070
- sel = prompt_with_quit(pt, bm, per_page: opts[:select_page_height])
1071
- return nil if sel.nil?
1072
-
1073
- ## store selected option
1074
- #
1075
- label_block = blocks_in_file.select do |fcb|
1076
- fcb[:label] == sel
1077
- end.fetch(0, nil)
1078
- opts[:block_name] = @options[:block_name] = label_block.fetch(:name, '')
1079
- end
1080
- approve_and_execute_block(opts, mdoc)
1081
- break unless repeat_menu
1319
+ loop do
1320
+ opts[:back] = false
1321
+ blocks_in_file, blocks_menu, mdoc = load_file_and_prepare_menu(opts)
1322
+
1323
+ unless opts[:block_name].present?
1324
+ block_name, state = wait_for_user_selection(blocks_in_file, blocks_menu, default,
1325
+ opts)
1326
+ case state
1327
+ when :exit
1328
+ return nil
1329
+ when :back
1330
+ opts[:block_name] = block_name[:option]
1331
+ opts[:back] = true
1332
+ when :continue
1333
+ opts[:block_name] = block_name
1334
+ end
1335
+ end
1336
+
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
1082
1340
 
1083
- opts[:block_name] = ''
1341
+ break if state == :continue && load_file == LOAD_FILE
1342
+ break unless repeat_menu
1343
+ end
1344
+ break if load_file != LOAD_FILE
1084
1345
  end
1085
1346
  rescue StandardError => err
1086
1347
  warn(error = "ERROR ** MarkParse.select_approve_and_execute_block(); #{err.inspect}")
@@ -1089,25 +1350,53 @@ module MarkdownExec
1089
1350
  raise ArgumentError, error
1090
1351
  end
1091
1352
 
1092
- def select_md_file(files = list_markdown_files_in_path)
1093
- opts = options
1094
- if (count = files.count) == 1
1095
- files[0]
1096
- elsif count >= 2
1097
- prompt_with_quit opts[:prompt_select_md].to_s, files,
1098
- 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 }
1099
1388
  end
1100
1389
  end
1101
1390
 
1102
1391
  def select_recent_output
1103
- filename = prompt_with_quit(
1392
+ filename = select_option_or_exit(
1104
1393
  @options[:prompt_select_output].to_s,
1105
1394
  list_recent_output(
1106
1395
  @options[:saved_stdout_folder],
1107
1396
  @options[:saved_stdout_glob],
1108
1397
  @options[:list_count]
1109
1398
  ),
1110
- { per_page: @options[:select_page_height] }
1399
+ @options.merge({ per_page: @options[:select_page_height] })
1111
1400
  )
1112
1401
  return unless filename.present?
1113
1402
 
@@ -1115,14 +1404,14 @@ module MarkdownExec
1115
1404
  end
1116
1405
 
1117
1406
  def select_recent_script
1118
- filename = prompt_with_quit(
1407
+ filename = select_option_or_exit(
1119
1408
  @options[:prompt_select_md].to_s,
1120
1409
  list_recent_scripts(
1121
1410
  @options[:saved_script_folder],
1122
1411
  @options[:saved_script_glob],
1123
1412
  @options[:list_count]
1124
1413
  ),
1125
- { per_page: @options[:select_page_height] }
1414
+ @options.merge({ per_page: @options[:select_page_height] })
1126
1415
  )
1127
1416
  return if filename.nil?
1128
1417
 
@@ -1135,11 +1424,37 @@ module MarkdownExec
1135
1424
  })
1136
1425
  end
1137
1426
 
1138
- def menu_export(data = menu_for_optparse)
1139
- data.map do |item|
1140
- item.delete(:procname)
1141
- item
1142
- 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]
1432
+ end
1433
+
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]))
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
1143
1458
  end
1144
1459
 
1145
1460
  def tab_completions(data = menu_for_optparse)
@@ -1148,6 +1463,77 @@ module MarkdownExec
1148
1463
  end.compact
1149
1464
  end
1150
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
+
1151
1537
  # :reek:BooleanParameter
1152
1538
  # :reek:ControlParameter
1153
1539
  def update_options(opts = {}, over: true)
@@ -1159,7 +1545,30 @@ module MarkdownExec
1159
1545
  @options
1160
1546
  end
1161
1547
 
1162
- def write_command_file(call_options, required_blocks)
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.
1571
+ def write_command_file(call_options, required_lines)
1163
1572
  return unless call_options[:save_executed_script]
1164
1573
 
1165
1574
  time_now = Time.now.utc
@@ -1174,24 +1583,113 @@ module MarkdownExec
1174
1583
  @options[:saved_filespec] =
1175
1584
  File.join opts[:saved_script_folder], opts[:saved_script_filename]
1176
1585
 
1177
- dirname = File.dirname(@options[:saved_filespec])
1178
- FileUtils.mkdir_p dirname
1179
1586
  shebang = if @options[:shebang]&.present?
1180
1587
  "#{@options[:shebang]} #{@options[:shell]}\n"
1181
1588
  else
1182
1589
  ''
1183
1590
  end
1184
1591
 
1185
- File.write(@options[:saved_filespec], shebang +
1186
- "# file_name: #{opts[:filename]}\n" \
1187
- "# block_name: #{opts[:block_name]}\n" \
1188
- "# time: #{time_now}\n" \
1189
- "#{required_blocks.flatten.join("\n")}\n")
1190
- 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"
1597
+
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
1191
1614
 
1192
- File.chmod @options[:saved_script_chmod], @options[:saved_filespec]
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
1193
1632
  end
1194
1633
  end # class MarkParse
1195
1634
  end # module MarkdownExec
1196
1635
 
1197
- require 'minitest/autorun' if $PROGRAM_NAME == __FILE__
1636
+ if $PROGRAM_NAME == __FILE__
1637
+ require 'bundler/setup'
1638
+ Bundler.require(:default)
1639
+
1640
+ require 'minitest/autorun'
1641
+
1642
+ module MarkdownExec
1643
+ class TestMarkParse < Minitest::Test
1644
+ require 'mocha/minitest'
1645
+
1646
+ def test_calling_execute_approved_block_calls_command_execute_with_argument_args_value
1647
+ pigeon = 'E'
1648
+ obj = { pass_args: pigeon }
1649
+
1650
+ c = MarkdownExec::MarkParse.new
1651
+
1652
+ # Expect that method command_execute is called with argument args having value pigeon
1653
+ c.expects(:command_execute).with(
1654
+ obj,
1655
+ '',
1656
+ args: pigeon
1657
+ )
1658
+
1659
+ # Call method execute_approved_block
1660
+ c.execute_approved_block(obj, [])
1661
+ end
1662
+
1663
+ def setup
1664
+ @mark_parse = MarkdownExec::MarkParse.new
1665
+ end
1666
+
1667
+ def test_set_fcb_title
1668
+ # sample input and output data for testing set_fcb_title method
1669
+ input_output_data = [
1670
+ {
1671
+ input: FCB.new(title: nil, body: ["puts 'Hello, world!'"]),
1672
+ output: "puts 'Hello, world!'"
1673
+ },
1674
+ {
1675
+ input: FCB.new(title: '', body: ['def add(x, y)', ' x + y', 'end']),
1676
+ output: 'def add(x, y) x + y end'
1677
+ },
1678
+ {
1679
+ input: FCB.new(title: 'foo', body: %w[bar baz]),
1680
+ output: 'foo' # expect the title to remain unchanged
1681
+ }
1682
+ ]
1683
+
1684
+ # iterate over the input and output data and
1685
+ # assert that the method sets the title as expected
1686
+ input_output_data.each do |data|
1687
+ input = data[:input]
1688
+ output = data[:output]
1689
+ @mark_parse.set_fcb_title(input)
1690
+ assert_equal output, input.title
1691
+ end
1692
+ end
1693
+ end
1694
+ end
1695
+ end