markdown_exec 1.3.9 → 1.4.1

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