markdown_exec 1.3.9 → 1.4

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