markdown_exec 1.6 → 1.7

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
@@ -18,8 +18,11 @@ require_relative 'cached_nested_file_reader'
18
18
  require_relative 'cli'
19
19
  require_relative 'colorize'
20
20
  require_relative 'env'
21
+ require_relative 'exceptions'
21
22
  require_relative 'fcb'
22
23
  require_relative 'filter'
24
+ require_relative 'fout'
25
+ require_relative 'hash_delegator'
23
26
  require_relative 'markdown_exec/version'
24
27
  require_relative 'mdoc'
25
28
  require_relative 'option_value'
@@ -42,79 +45,20 @@ MDE_HISTORY_ENV_NAME = 'MDE_MENU_HISTORY'
42
45
  #
43
46
  class FileMissingError < StandardError; end
44
47
 
45
- # hash with keys sorted by name
46
- # add Hash.sym_keys
47
- #
48
- class Hash
49
- unless defined?(sort_by_key)
50
- def sort_by_key
51
- keys.sort.to_h { |key| [key, self[key]] }
52
- end
53
- end
54
-
55
- unless defined?(sym_keys)
56
- def sym_keys
57
- transform_keys(&:to_sym)
58
- end
59
- end
60
- end
61
-
62
- class LoadFile
63
- Load = true
64
- Reuse = false
65
- end
66
-
67
- class MenuState
68
- BACK = :back
69
- CONTINUE = :continue
70
- EXIT = :exit
71
- end
72
-
73
- # integer value for comparison
74
- #
75
- def options_fetch_display_level(options)
76
- options.fetch(:display_level, 1)
77
- end
78
-
79
- # integer value for comparison
80
- #
81
- def options_fetch_display_level_xbase_prefix(options)
82
- options.fetch(:level_xbase_prefix, '')
48
+ def dp(str)
49
+ lout " => #{str}", level: DISPLAY_LEVEL_DEBUG
83
50
  end
84
51
 
85
- # stdout manager
86
- #
87
- module FOUT
88
- # standard output; not for debug
89
- #
90
- def fout(str)
91
- puts str
92
- end
93
-
94
- def fout_list(str)
95
- puts str
96
- end
97
-
98
- def fout_section(name, data)
99
- puts "# #{name}"
100
- puts data.to_yaml
101
- end
102
-
103
- def approved_fout?(level)
104
- level <= options_fetch_display_level(@options)
105
- end
106
-
107
- # display output at level or lower than filter (DISPLAY_LEVEL_DEFAULT)
108
- #
109
- def lout(str, level: DISPLAY_LEVEL_BASE)
110
- return unless approved_fout? level
111
-
112
- fout level == DISPLAY_LEVEL_BASE ? str : options_fetch_display_level_xbase_prefix(@options) + str
113
- end
52
+ def rbp
53
+ rpry
54
+ pp(caller.take(4).map.with_index { |line, ind| " - #{ind}: #{line}" })
55
+ binding.pry
114
56
  end
115
57
 
116
- def dp(str)
117
- lout " => #{str}", level: DISPLAY_LEVEL_DEBUG
58
+ def bpp(*args)
59
+ pp '+ bpp()'
60
+ pp(*args.map.with_index { |line, ind| " - #{ind}: #{line}" })
61
+ rbp
118
62
  end
119
63
 
120
64
  def rpry
@@ -124,6 +68,13 @@ end
124
68
 
125
69
  public
126
70
 
71
+ # convert regex match groups to a hash with symbol keys
72
+ #
73
+ # :reek:UtilityFunction
74
+ def extract_named_captures_from_option(str, option)
75
+ str.match(Regexp.new(option))&.named_captures&.sym_keys
76
+ end
77
+
127
78
  # :reek:UtilityFunction
128
79
  def list_recent_output(saved_stdout_folder, saved_stdout_glob,
129
80
  list_count)
@@ -138,61 +89,10 @@ def list_recent_scripts(saved_script_folder, saved_script_glob,
138
89
  saved_script_glob, list_count)
139
90
  end
140
91
 
141
- # convert regex match groups to a hash with symbol keys
142
- #
143
- # :reek:UtilityFunction
144
- def extract_named_captures_from_option(str, option)
145
- str.match(Regexp.new(option))&.named_captures&.sym_keys
146
- end
147
-
148
- module ArrayUtil
149
- def self.partition_by_predicate(arr)
150
- true_list = []
151
- false_list = []
152
-
153
- arr.each do |element|
154
- if yield(element)
155
- true_list << element
156
- else
157
- false_list << element
158
- end
159
- end
160
-
161
- [true_list, false_list]
162
- end
163
- end
164
-
165
- module StringUtil
166
- # Splits the given string on the first occurrence of the specified character.
167
- # Returns an array containing the portion of the string before the character and the rest of the string.
168
- #
169
- # @param input_str [String] The string to be split.
170
- # @param split_char [String] The character on which to split the string.
171
- # @return [Array<String>] An array containing two elements: the part of the string before split_char, and the rest of the string.
172
- def self.partition_at_first(input_str, split_char)
173
- split_index = input_str.index(split_char)
174
-
175
- if split_index.nil?
176
- [input_str, '']
177
- else
178
- [input_str[0...split_index], input_str[(split_index + 1)..-1]]
179
- end
180
- end
181
- end
182
-
183
92
  # execute markdown documents
184
93
  #
185
94
  module MarkdownExec
186
- # :reek:IrresponsibleModule
187
- FNR11 = '/'
188
- FNR12 = ',~'
189
-
190
- SHELL_COLOR_OPTIONS = {
191
- BlockType::BASH => :menu_bash_color,
192
- BlockType::LINK => :menu_link_color,
193
- BlockType::OPTS => :menu_opts_color,
194
- BlockType::VARS => :menu_vars_color
195
- }.freeze
95
+ include Exceptions
196
96
 
197
97
  ##
198
98
  #
@@ -203,124 +103,32 @@ module MarkdownExec
203
103
  # :reek:TooManyInstanceVariables ### temp
204
104
  # :reek:TooManyMethods ### temp
205
105
  class MarkParse
206
- attr_reader :options
106
+ attr_reader :options, :prompt, :run_state
207
107
 
208
108
  include ArrayUtil
209
109
  include StringUtil
210
- include FOUT
211
110
 
212
111
  def initialize(options = {})
213
- @execute_aborted_at = nil
214
- @execute_completed_at = nil
215
- @execute_error = nil
216
- @execute_error_message = nil
217
- @execute_files = nil
218
- @execute_options = nil
219
- @execute_script_filespec = nil
220
- @execute_started_at = nil
221
112
  @option_parser = nil
222
- @options = options
223
- @prompt = tty_prompt_without_disabled_symbol
224
- end
225
-
226
- # Adds Back and Exit options to the CLI menu
227
- #
228
- # @param blocks_in_file [Array] The current blocks in the menu
229
- def add_menu_chrome_blocks!(blocks_in_file)
230
- return unless @options[:menu_link_format].present?
231
113
 
232
- if @options[:menu_with_back] && history_state_exist?
233
- append_chrome_block(blocks_in_file, MenuState::BACK)
234
- end
235
- if @options[:menu_with_exit]
236
- append_chrome_block(blocks_in_file, MenuState::EXIT)
237
- end
238
- append_divider(blocks_in_file, @options, :initial)
239
- append_divider(blocks_in_file, @options, :final)
114
+ @options = HashDelegator.new(options)
115
+ @fout = FOut.new(@delegate_object)
240
116
  end
241
117
 
242
- ##
243
- # Appends a summary of a block (FCB) to the blocks array.
244
- #
245
- def append_block_summary(blocks, fcb, opts)
246
- ## enhance fcb with block summary
247
- #
248
- blocks.push get_block_summary(opts, fcb)
249
- end
250
-
251
- # Appends a chrome block, which is a menu option for Back or Exit
252
- #
253
- # @param blocks_in_file [Array] The current blocks in the menu
254
- # @param type [Symbol] The type of chrome block to add (:back or :exit)
255
- def append_chrome_block(blocks_in_file, type)
256
- case type
257
- when MenuState::BACK
258
- state = history_state_partition(@options)
259
- @hs_curr = state[:unit]
260
- @hs_rest = state[:rest]
261
- option_name = @options[:menu_option_back_name]
262
- insert_at_top = @options[:menu_back_at_top]
263
- when MenuState::EXIT
264
- option_name = @options[:menu_option_exit_name]
265
- insert_at_top = @options[:menu_exit_at_top]
266
- end
118
+ private
267
119
 
268
- formatted_name = format(@options[:menu_link_format],
269
- safeval(option_name))
270
- chrome_block = FCB.new(
271
- chrome: true,
272
- dname: formatted_name.send(@options[:menu_link_color].to_sym),
273
- oname: formatted_name
120
+ def error_handler(name = '', opts = {})
121
+ Exceptions.error_handler(
122
+ "CachedNestedFileReader.#{name} -- #{$!}",
123
+ opts
274
124
  )
275
-
276
- if insert_at_top
277
- blocks_in_file.unshift(chrome_block)
278
- else
279
- blocks_in_file.push(chrome_block)
280
- end
281
125
  end
282
126
 
283
- # Appends a divider to the blocks array.
284
- # @param blocks [Array] The array of block elements.
285
- # @param opts [Hash] Options containing divider configuration.
286
- # @param position [Symbol] :initial or :final divider position.
287
- def append_divider(blocks, opts, position)
288
- divider_key = position == :initial ? :menu_initial_divider : :menu_final_divider
289
- unless opts[:menu_divider_format].present? && opts[divider_key].present?
290
- return
291
- end
292
-
293
- oname = format(opts[:menu_divider_format],
294
- safeval(opts[divider_key]))
295
- divider = FCB.new(
296
- chrome: true,
297
- disabled: '',
298
- dname: oname.send(opts[:menu_divider_color].to_sym),
299
- oname: oname
127
+ def warn_format(name, message, opts = {})
128
+ Exceptions.warn_format(
129
+ "CachedNestedFileReader.#{name} -- #{message}",
130
+ opts
300
131
  )
301
-
302
- position == :initial ? blocks.unshift(divider) : blocks.push(divider)
303
- end
304
-
305
- # Execute a code block after approval and provide user interaction options.
306
- #
307
- # This method displays required code blocks, asks for user approval, and
308
- # executes the code block if approved. It also allows users to copy the
309
- # code to the clipboard or save it to a file.
310
- #
311
- # @param opts [Hash] Options hash containing configuration settings.
312
- # @param mdoc [YourMDocClass] An instance of the MDoc class.
313
- #
314
- def approve_and_execute_block(selected, opts, mdoc)
315
- if selected.fetch(:shell, '') == BlockType::LINK
316
- handle_shell_link(opts, selected.fetch(:body, ''), mdoc)
317
- elsif opts.fetch(:s_back, false)
318
- handle_back_link(opts)
319
- elsif selected[:shell] == BlockType::OPTS
320
- handle_shell_opts(opts, selected)
321
- else
322
- handle_remainder_blocks(mdoc, opts, selected)
323
- end
324
132
  end
325
133
 
326
134
  # return arguments before `--`
@@ -355,249 +163,34 @@ module MarkdownExec
355
163
  end.compact.to_h
356
164
  end
357
165
 
358
- # Finds the first hash-like element within an enumerable collection where the specified key
359
- # matches the given value. Returns a default value if no match is found.
360
- #
361
- # @param blocks [Enumerable] An enumerable collection of hash-like objects.
362
- # @param key [Object] The key to look up in each hash-like object.
363
- # @param value [Object] The value to compare against the value associated with the key.
364
- # @param default [Object] The default value to return if no match is found (optional).
365
- # @return [Object, nil] The found hash-like object, or the default value if no match is found.
366
- def block_find(blocks, key, value, default = nil)
367
- blocks.find { |item| item[key] == value } || default
368
- end
369
-
370
- def blocks_per_opts(blocks, opts)
371
- return blocks if opts[:struct]
372
-
373
- blocks.map do |block|
374
- block.fetch(:text, nil) || block.oname
375
- end.compact.reject(&:empty?)
376
- end
377
-
378
166
  def calculated_options
379
167
  {
380
168
  bash: true, # bash block parsing in get_block_summary()
381
- saved_script_filename: nil, # calculated
382
- struct: true # allow get_block_summary()
169
+ saved_script_filename: nil # calculated
383
170
  }
384
171
  end
385
172
 
386
- # Check whether the document exists and is readable
387
- def check_file_existence(filename)
388
- unless filename&.present?
389
- fout 'No blocks found.'
390
- return false
391
- end
392
-
393
- unless File.exist? filename
394
- fout 'Document is missing.'
395
- return false
396
- end
397
- true
398
- end
399
-
400
173
  def clear_required_file
401
174
  ENV['MDE_LINK_REQUIRED_FILE'] = ''
402
175
  end
403
176
 
404
- # Collect required code blocks based on the provided options.
405
- #
406
- # @param opts [Hash] Options hash containing configuration settings.
407
- # @param mdoc [YourMDocClass] An instance of the MDoc class.
408
- # @return [Array<String>] Required code blocks as an array of lines.
409
- def collect_required_code_lines(mdoc, selected, opts: {})
410
- # Apply hash in opts block to environment variables
411
- if selected[:shell] == BlockType::VARS
412
- data = YAML.load(selected[:body].join("\n"))
413
- data.each_key do |key|
414
- ENV[key] = value = data[key].to_s
415
- next unless opts[:menu_vars_set_format].present?
416
-
417
- print format(
418
- opts[:menu_vars_set_format],
419
- { key: key,
420
- value: value }
421
- ).send(opts[:menu_vars_set_color].to_sym)
422
- end
423
- end
424
-
425
- required = mdoc.collect_recursively_required_code(opts[:block_name],
426
- opts: opts)
427
- read_required_blocks_from_temp_file + required[:code]
428
- end
429
-
430
- def cfile
431
- @cfile ||= CachedNestedFileReader.new(
432
- import_pattern: @options.fetch(:import_pattern)
433
- )
434
- end
435
-
436
- EF_STDOUT = :stdout
437
- EF_STDERR = :stderr
438
- EF_STDIN = :stdin
439
-
440
- # Existing command_execute method
441
- def command_execute(opts, command, args: [])
442
- @execute_files = Hash.new([])
443
- @execute_options = opts
444
- @execute_started_at = Time.now.utc
445
-
446
- Open3.popen3(opts[:shell], '-c', command, opts[:filename],
447
- *args) do |stdin, stdout, stderr, exec_thr|
448
- handle_stream(opts, stdout, EF_STDOUT) do |line|
449
- yield nil, line, nil, exec_thr if block_given?
450
- end
451
- handle_stream(opts, stderr, EF_STDERR) do |line|
452
- yield nil, nil, line, exec_thr if block_given?
453
- end
454
-
455
- in_thr = handle_stream(opts, $stdin, EF_STDIN) do |line|
456
- stdin.puts(line)
457
- yield line, nil, nil, exec_thr if block_given?
458
- end
459
-
460
- exec_thr.join
461
- sleep 0.1
462
- in_thr.kill if in_thr&.alive?
463
- end
464
-
465
- @execute_completed_at = Time.now.utc
466
- rescue Errno::ENOENT => err
467
- #d 'command error ENOENT triggered by missing command in script'
468
- @execute_aborted_at = Time.now.utc
469
- @execute_error_message = err.message
470
- @execute_error = err
471
- @execute_files[EF_STDERR] += [@execute_error_message]
472
- fout "Error ENOENT: #{err.inspect}"
473
- rescue SignalException => err
474
- #d 'command SIGTERM triggered by user or system'
475
- @execute_aborted_at = Time.now.utc
476
- @execute_error_message = 'SIGTERM'
477
- @execute_error = err
478
- @execute_files[EF_STDERR] += [@execute_error_message]
479
- fout "Error ENOENT: #{err.inspect}"
480
- end
481
-
482
- def command_or_user_selected_block(blocks_in_file, blocks_menu, default,
483
- opts)
484
- if opts[:block_name].present?
485
- block = blocks_in_file.find do |item|
486
- item[:oname] == opts[:block_name]
487
- end
488
- else
489
- block, state = wait_for_user_selected_block(blocks_in_file, blocks_menu, default,
490
- opts)
491
- end
492
-
493
- [block, state]
494
- end
495
-
496
- def copy_to_clipboard(required_lines)
497
- text = required_lines.flatten.join($INPUT_RECORD_SEPARATOR)
498
- Clipboard.copy(text)
499
- fout "Clipboard updated: #{required_lines.count} blocks," \
500
- " #{required_lines.flatten.count} lines," \
501
- " #{text.length} characters"
502
- end
503
-
504
- def count_blocks_in_filename
505
- fenced_start_and_end_regex = Regexp.new @options[:fenced_start_and_end_regex]
506
- cnt = 0
507
- cfile.readlines(@options[:filename]).each do |line|
508
- cnt += 1 if line.match(fenced_start_and_end_regex)
509
- end
510
- cnt / 2
511
- end
512
-
513
- ##
514
- # Creates and adds a formatted block to the blocks array based on the provided match and format options.
515
- # @param blocks [Array] The array of blocks to add the new block to.
516
- # @param fcb [FCB] The file control block containing the line to match against.
517
- # @param match_data [MatchData] The match data containing named captures for formatting.
518
- # @param format_option [String] The format string to be used for the new block.
519
- # @param color_method [Symbol] The color method to apply to the block's display name.
520
- def create_and_add_chrome_block(blocks, _fcb, match_data, format_option,
521
- color_method)
522
- oname = format(format_option,
523
- match_data.named_captures.transform_keys(&:to_sym))
524
- blocks.push FCB.new(
525
- chrome: true,
526
- disabled: '',
527
- dname: oname.send(color_method),
528
- oname: oname
529
- )
530
- end
531
-
532
- ##
533
- # Processes lines within the file and converts them into blocks if they match certain criteria.
534
- # @param blocks [Array] The array to append new blocks to.
535
- # @param fcb [FCB] The file control block being processed.
536
- # @param opts [Hash] Options containing configuration for line processing.
537
- # @param use_chrome [Boolean] Indicates if the chrome styling should be applied.
538
- def create_and_add_chrome_blocks(blocks, fcb, opts, use_chrome)
539
- return unless use_chrome
540
-
541
- match_criteria = [
542
- { match: :menu_task_match, format: :menu_task_format,
543
- color: :menu_task_color },
544
- { match: :menu_divider_match, format: :menu_divider_format,
545
- color: :menu_divider_color },
546
- { match: :menu_note_match, format: :menu_note_format,
547
- color: :menu_note_color }
548
- ]
549
-
550
- match_criteria.each do |criteria|
551
- unless opts[criteria[:match]].present? &&
552
- (mbody = fcb.body[0].match opts[criteria[:match]])
553
- next
554
- end
555
-
556
- create_and_add_chrome_block(blocks, fcb, mbody, opts[criteria[:format]],
557
- opts[criteria[:color]].to_sym)
558
- break
559
- end
560
- end
561
-
562
- def create_and_write_file_with_permissions(file_path, content,
563
- chmod_value)
564
- dirname = File.dirname(file_path)
565
- FileUtils.mkdir_p dirname
566
- File.write(file_path, content)
567
- return if chmod_value.zero?
177
+ # # Deletes a required temporary file specified by an environment variable.
178
+ # # The function checks if the file exists before attempting to delete it.
179
+ # # Clears the environment variable after deletion.
180
+ # #
181
+ # def delete_required_temp_file
182
+ # temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
568
183
 
569
- File.chmod chmod_value, file_path
570
- end
184
+ # return if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
571
185
 
572
- # Deletes a required temporary file specified by an environment variable.
573
- # The function checks if the file exists before attempting to delete it.
574
- # Clears the environment variable after deletion.
575
- #
576
- def delete_required_temp_file
577
- temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
186
+ # FileUtils.rm_f(temp_blocks_file_path)
578
187
 
579
- if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
580
- return
581
- end
188
+ # clear_required_file
189
+ # rescue StandardError
190
+ # error_handler('delete_required_temp_file')
191
+ # end
582
192
 
583
- FileUtils.rm_f(temp_blocks_file_path)
584
-
585
- clear_required_file
586
- end
587
-
588
- # Derives a title from the body of an FCB object.
589
- # @param fcb [Object] The FCB object whose title is to be derived.
590
- # @return [String] The derived title.
591
- def derive_title_from_body(fcb)
592
- body_content = fcb&.body
593
- return '' unless body_content
594
-
595
- if body_content.count == 1
596
- body_content.first
597
- else
598
- format_multiline_body_as_title(body_content)
599
- end
600
- end
193
+ public
601
194
 
602
195
  ## Determines the correct filename to use for searching files
603
196
  #
@@ -606,8 +199,7 @@ module MarkdownExec
606
199
  if specified_filename&.present?
607
200
  return specified_filename if specified_filename.start_with?('/')
608
201
 
609
- File.join(specified_folder || default_folder,
610
- specified_filename)
202
+ File.join(specified_folder || default_folder, specified_filename)
611
203
  elsif specified_folder&.present?
612
204
  File.join(specified_folder,
613
205
  filetree ? @options[:md_filename_match] : @options[:md_filename_glob])
@@ -616,31 +208,19 @@ module MarkdownExec
616
208
  end
617
209
  end
618
210
 
619
- # :reek:DuplicateMethodCall
620
- def display_required_code(opts, required_lines)
621
- frame = opts[:output_divider].send(opts[:output_divider_color].to_sym)
622
- fout frame
623
- required_lines.each { |cb| fout cb }
624
- fout frame
625
- end
211
+ private
626
212
 
627
- def execute_approved_block(opts, required_lines)
628
- write_command_file(opts, required_lines)
629
- command_execute(
630
- opts,
631
- required_lines.flatten.join("\n"),
632
- args: opts.fetch(:s_pass_args, [])
633
- )
634
- initialize_and_save_execution_output
635
- output_execution_summary
636
- output_execution_result
637
- end
213
+ # def error_handler(name = '', event = nil, backtrace = nil)
214
+ # warn(error = "\n * ERROR * #{name}; #{$!.inspect}")
215
+ # warn($@.take(4).map.with_index { |line, ind| " * #{ind}: #{line}" })
216
+ # binding.pry if $tap_enable
217
+ # raise ArgumentError, error
218
+ # end
638
219
 
639
220
  # Reports and executes block logic
640
221
  def execute_block_logic(files)
641
222
  @options[:filename] = select_document_if_multiple(files)
642
- select_approve_and_execute_block({ bash: true,
643
- struct: true })
223
+ @options.select_approve_and_execute_block
644
224
  end
645
225
 
646
226
  ## Executes the block specified in the options
@@ -649,12 +229,10 @@ module MarkdownExec
649
229
  finalize_cli_argument_processing(rest)
650
230
  @options[:s_cli_rest] = rest
651
231
  execute_code_block_based_on_options(@options)
652
- rescue FileMissingError => err
653
- puts "File missing: #{err}"
654
- rescue StandardError => err
655
- warn(error = "ERROR ** MarkParse.execute_block_with_error_handling(); #{err.inspect}")
656
- binding.pry if $tap_enable
657
- raise ArgumentError, error
232
+ rescue FileMissingError
233
+ warn "File missing: #{$!}"
234
+ rescue StandardError
235
+ error_handler('execute_block_with_error_handling')
658
236
  end
659
237
 
660
238
  # Main method to execute a block based on options and block_name
@@ -663,47 +241,40 @@ module MarkdownExec
663
241
  update_options(options, over: false)
664
242
 
665
243
  simple_commands = {
666
- doc_glob: -> { fout options[:md_filename_glob] },
667
- list_blocks: lambda do
668
- fout_list (files.map do |file|
669
- menu_with_block_labels(filename: file,
670
- struct: true)
671
- end).flatten(1)
672
- end,
673
- list_default_yaml: -> { fout_list list_default_yaml },
674
- list_docs: -> { fout_list files },
675
- list_default_env: -> { fout_list list_default_env },
244
+ doc_glob: -> { @fout.fout options[:md_filename_glob] },
245
+ list_default_yaml: -> { @fout.fout_list list_default_yaml },
246
+ list_docs: -> { @fout.fout_list files },
247
+ list_default_env: -> { @fout.fout_list list_default_env },
676
248
  list_recent_output: lambda {
677
- fout_list list_recent_output(
249
+ @fout.fout_list list_recent_output(
678
250
  @options[:saved_stdout_folder],
679
251
  @options[:saved_stdout_glob], @options[:list_count]
680
252
  )
681
253
  },
682
254
  list_recent_scripts: lambda {
683
- fout_list list_recent_scripts(
255
+ @fout.fout_list list_recent_scripts(
684
256
  options[:saved_script_folder],
685
257
  options[:saved_script_glob], options[:list_count]
686
258
  )
687
259
  },
688
- pwd: -> { fout File.expand_path('..', __dir__) },
260
+ pwd: -> { @fout.fout File.expand_path('..', __dir__) },
689
261
  run_last_script: -> { run_last_script },
690
262
  select_recent_output: -> { select_recent_output },
691
263
  select_recent_script: -> { select_recent_script },
692
- tab_completions: -> { fout tab_completions },
693
- menu_export: -> { fout menu_export }
264
+ tab_completions: -> { @fout.fout tab_completions },
265
+ menu_export: -> { @fout.fout menu_export }
694
266
  }
695
267
 
696
268
  return if execute_simple_commands(simple_commands)
697
269
 
698
- files = prepare_file_list(options)
270
+ files = opts_prepare_file_list(options)
699
271
  execute_block_logic(files)
700
272
  return unless @options[:output_saved_script_filename]
701
273
 
702
- fout "saved_filespec: #{@execute_script_filespec}"
703
- rescue StandardError => err
704
- warn(error = "ERROR ** MarkParse.execute_code_block_based_on_options(); #{err.inspect}")
705
- binding.pry if $tap_enable
706
- raise ArgumentError, error
274
+ @fout.fout "script_block_name: #{@options.run_state.script_block_name}"
275
+ @fout.fout "s_save_filespec: #{@options.run_state.saved_filespec}"
276
+ rescue StandardError
277
+ error_handler('execute_code_block_based_on_options')
707
278
  end
708
279
 
709
280
  # Executes command based on the provided option keys
@@ -736,187 +307,22 @@ module MarkdownExec
736
307
  #
737
308
  block_name = rest.shift
738
309
  @options[:block_name] = block_name if block_name.present?
739
- end
740
-
741
- # Formats multiline body content as a title string.
742
- # indents all but first line with two spaces so it displays correctly in menu
743
- # @param body_lines [Array<String>] The lines of body content.
744
- # @return [String] Formatted title.
745
- def format_multiline_body_as_title(body_lines)
746
- body_lines.map.with_index do |line, index|
747
- index.zero? ? line : " #{line}"
748
- end.join("\n") << "\n"
749
- end
750
-
751
- ## summarize blocks
752
- #
753
- def get_block_summary(call_options, fcb)
754
- opts = optsmerge call_options
755
- # return fcb.body unless opts[:struct]
756
- return fcb unless opts[:bash]
757
-
758
- fcb.call = fcb.title.match(Regexp.new(opts[:block_calls_scan]))&.fetch(1, nil)
759
- titlexcall = if fcb.call
760
- fcb.title.sub("%#{fcb.call}", '')
761
- else
762
- fcb.title
763
- end
764
- bm = extract_named_captures_from_option(titlexcall,
765
- opts[:block_name_match])
766
- fcb.stdin = extract_named_captures_from_option(titlexcall,
767
- opts[:block_stdin_scan])
768
- fcb.stdout = extract_named_captures_from_option(titlexcall,
769
- opts[:block_stdout_scan])
770
-
771
- shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
772
- fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall
773
- fcb.dname = if shell_color_option && opts[shell_color_option].present?
774
- fcb.oname.send(opts[shell_color_option].to_sym)
775
- else
776
- fcb.oname
777
- end
778
- fcb
779
- end
780
-
781
- # Handles the link-back operation.
782
- #
783
- # @param opts [Hash] Configuration options hash.
784
- # @return [Array<Symbol, String>] A tuple containing a LoadFile flag and an empty string.
785
- def handle_back_link(opts)
786
- history_state_pop(opts)
787
- [LoadFile::Load, '']
788
- end
789
-
790
- # Handles the execution and display of remainder blocks from a selected menu item.
791
- #
792
- # @param mdoc [Object] Document object containing code blocks.
793
- # @param opts [Hash] Configuration options hash.
794
- # @param selected [Hash] Selected item from the menu.
795
- # @return [Array<Symbol, String>] A tuple containing a LoadFile flag and an empty string.
796
- # @note The function can prompt the user for approval before executing code if opts[:user_must_approve] is true.
797
- def handle_remainder_blocks(mdoc, opts, selected)
798
- required_lines = collect_required_code_lines(mdoc, selected,
799
- opts: opts)
800
- if opts[:output_script] || opts[:user_must_approve]
801
- display_required_code(opts, required_lines)
802
- end
803
- allow = if opts[:user_must_approve]
804
- prompt_for_user_approval(opts,
805
- required_lines)
806
- else
807
- true
808
- end
809
- opts[:s_ir_approve] = allow
810
- if opts[:s_ir_approve]
811
- execute_approved_block(opts,
812
- required_lines)
813
- end
814
-
815
- [LoadFile::Reuse, '']
816
- end
817
-
818
- # Handles the link-shell operation.
819
- #
820
- # @param opts [Hash] Configuration options hash.
821
- # @param body [Array<String>] The body content.
822
- # @param mdoc [Object] Document object containing code blocks.
823
- # @return [Array<Symbol, String>] A tuple containing a LoadFile flag and a block name.
824
- def handle_shell_link(opts, body, mdoc)
825
- data = body.present? ? YAML.load(body.join("\n")) : {}
826
- data_file = data.fetch('file', nil)
827
- return [LoadFile::Reuse, ''] unless data_file
828
-
829
- history_state_push(mdoc, data_file, opts)
830
-
831
- data.fetch('vars', []).each do |var|
832
- ENV[var[0]] = var[1].to_s
833
- end
834
-
835
- [LoadFile::Load, data.fetch('block', '')]
836
- end
837
-
838
- # Handles options for the shell.
839
- #
840
- # @param opts [Hash] Configuration options hash.
841
- # @param selected [Hash] Selected item from the menu.
842
- # @return [Array<Symbol, String>] A tuple containing a LoadFile::Reuse flag and an empty string.
843
- def handle_shell_opts(opts, selected, tgt2 = nil)
844
- data = YAML.load(selected[:body].join("\n"))
845
- data.each_key do |key|
846
- opts[key.to_sym] = value = data[key]
847
- tgt2[key.to_sym] = value if tgt2
848
- next unless opts[:menu_opts_set_format].present?
849
-
850
- print format(
851
- opts[:menu_opts_set_format],
852
- { key: key,
853
- value: value }
854
- ).send(opts[:menu_opts_set_color].to_sym)
855
- end
856
- [LoadFile::Reuse, '']
857
- end
858
-
859
- # Handles reading and processing lines from a given IO stream
860
- #
861
- # @param stream [IO] The IO stream to read from (e.g., stdout, stderr, stdin).
862
- # @param file_type [Symbol] The type of file to which the stream corresponds.
863
- def handle_stream(opts, stream, file_type, swap: false)
864
- Thread.new do
865
- until (line = stream.gets).nil?
866
- @execute_files[file_type] =
867
- @execute_files[file_type] + [line.strip]
868
- print line if opts[:output_stdout]
869
- yield line if block_given?
870
- end
871
- rescue IOError
872
- #d 'stdout IOError, thread killed, do nothing'
873
- end
874
- end
875
-
876
- def history_state_exist?
877
- history = ENV.fetch(MDE_HISTORY_ENV_NAME, '')
878
- history.present? ? history : nil
879
- end
880
-
881
- def history_state_partition(opts)
882
- unit, rest = StringUtil.partition_at_first(
883
- ENV.fetch(MDE_HISTORY_ENV_NAME, ''),
884
- opts[:history_document_separator]
885
- )
886
- { unit: unit, rest: rest }.tap_inspect
887
- end
888
-
889
- def history_state_pop(opts)
890
- state = history_state_partition(opts)
891
- opts[:filename] = state[:unit]
892
- ENV[MDE_HISTORY_ENV_NAME] = state[:rest]
893
- delete_required_temp_file
894
- end
895
-
896
- def history_state_push(mdoc, data_file, opts)
897
- [data_file, opts[:block_name]].tap_inspect 'filename, blockname'
898
- new_history = opts[:filename] +
899
- opts[:history_document_separator] +
900
- ENV.fetch(MDE_HISTORY_ENV_NAME, '')
901
- opts[:filename] = data_file
902
- write_required_blocks_to_temp_file(mdoc, opts[:block_name], opts)
903
- ENV[MDE_HISTORY_ENV_NAME] = new_history
904
- end
905
-
906
- # Indents all lines in a given string with a specified indentation string.
907
- # @param body [String] A multi-line string to be indented.
908
- # @param indent [String] The string used for indentation (default is an empty string).
909
- # @return [String] A single string with each line indented as specified.
910
- def indent_all_lines(body, indent = nil)
911
- return body if !indent.present?
912
-
913
- body.lines.map { |line| indent + line.chomp }.join("\n")
310
+ rescue FileMissingError
311
+ warn_format('finalize_cli_argument_processing',
312
+ "File missing -- #{$!}", { abort: true })
313
+ # @options[:block_name] = ''
314
+ # @options[:filename] = ''
315
+ # exit 1
316
+ rescue StandardError
317
+ error_handler('finalize_cli_argument_processing')
914
318
  end
915
319
 
916
320
  ## Sets up the options and returns the parsed arguments
917
321
  #
918
322
  def initialize_and_parse_cli_options
919
- @options = base_options
323
+ # @options = base_options
324
+ @options = HashDelegator.new(base_options)
325
+
920
326
  read_configuration_file!(@options,
921
327
  ".#{MarkdownExec::APP_NAME.downcase}.yml")
922
328
 
@@ -929,7 +335,7 @@ module MarkdownExec
929
335
  ].join("\n")
930
336
 
931
337
  menu_iter do |item|
932
- menu_option_append opts, @options, item
338
+ opts_menu_option_append opts, @options, item
933
339
  end
934
340
  end
935
341
  @option_parser.load
@@ -937,55 +343,11 @@ module MarkdownExec
937
343
 
938
344
  rest = @option_parser.parse!(arguments_for_mde)
939
345
  @options[:s_pass_args] = ARGV[rest.count + 1..]
346
+ @options.merge(@options.run_state.to_h)
940
347
 
941
348
  rest
942
349
  end
943
350
 
944
- def initialize_and_save_execution_output
945
- return unless @options[:save_execution_output]
946
-
947
- @options[:logged_stdout_filename] =
948
- SavedAsset.stdout_name(blockname: @options[:block_name],
949
- filename: File.basename(@options[:filename],
950
- '.*'),
951
- prefix: @options[:logged_stdout_filename_prefix],
952
- time: Time.now.utc)
953
-
954
- @logged_stdout_filespec =
955
- @options[:logged_stdout_filespec] =
956
- File.join @options[:saved_stdout_folder],
957
- @options[:logged_stdout_filename]
958
- @logged_stdout_filespec = @options[:logged_stdout_filespec]
959
- write_execution_output_to_file
960
- end
961
-
962
- # Initializes variables for regex and other states
963
- def initialize_state(opts)
964
- {
965
- fenced_start_and_end_regex: Regexp.new(opts[:fenced_start_and_end_regex]),
966
- fenced_start_extended_regex: Regexp.new(opts[:fenced_start_extended_regex]),
967
- fcb: FCB.new,
968
- in_fenced_block: false,
969
- headings: []
970
- }
971
- end
972
-
973
- # Main function to iterate through blocks in file
974
- def iter_blocks_in_file(opts = {}, &block)
975
- return unless check_file_existence(opts[:filename])
976
-
977
- state = initialize_state(opts)
978
-
979
- selected_messages = yield :filter
980
-
981
- cfile.readlines(opts[:filename]).each do |line|
982
- next unless line
983
-
984
- update_line_and_block_state(line, state, opts, selected_messages,
985
- &block)
986
- end
987
- end
988
-
989
351
  ##
990
352
  # Returns a lambda expression based on the given procname.
991
353
  # @param procname [String] The name of the process to generate a lambda for.
@@ -1001,7 +363,7 @@ module MarkdownExec
1001
363
  ->(_) { exit }
1002
364
  when 'help'
1003
365
  lambda { |_|
1004
- fout menu_help
366
+ @fout.fout menu_help
1005
367
  exit
1006
368
  }
1007
369
  when 'path'
@@ -1009,7 +371,7 @@ module MarkdownExec
1009
371
  when 'show_config'
1010
372
  lambda { |_|
1011
373
  finalize_cli_argument_processing(options)
1012
- fout options.sort_by_key.to_yaml
374
+ @fout.fout options.sort_by_key.to_yaml
1013
375
  }
1014
376
  when 'val_as_bool'
1015
377
  lambda { |value|
@@ -1021,7 +383,7 @@ module MarkdownExec
1021
383
  ->(value) { value.to_s }
1022
384
  when 'version'
1023
385
  lambda { |_|
1024
- fout MarkdownExec::VERSION
386
+ @fout.fout MarkdownExec::VERSION
1025
387
  exit
1026
388
  }
1027
389
  else
@@ -1051,16 +413,7 @@ module MarkdownExec
1051
413
  end.compact.sort
1052
414
  end
1053
415
 
1054
- def list_files_per_options(options)
1055
- list_files_specified(
1056
- determine_filename(
1057
- specified_filename: options[:filename]&.present? ? options[:filename] : nil,
1058
- specified_folder: options[:path],
1059
- default_filename: 'README.md',
1060
- default_folder: '.'
1061
- )
1062
- )
1063
- end
416
+ public
1064
417
 
1065
418
  ## Searches for files based on the specified or default filenames and folders
1066
419
  #
@@ -1077,80 +430,7 @@ module MarkdownExec
1077
430
  @options[:md_filename_glob]))
1078
431
  end
1079
432
 
1080
- ## output type (body string or full object) per option struct and bash
1081
- #
1082
- def list_named_blocks_in_file(call_options = {}, &options_block)
1083
- opts = optsmerge call_options, options_block
1084
- blocks_per_opts(
1085
- menu_from_file(opts.merge(struct: true)).select do |fcb|
1086
- Filter.fcb_select?(opts.merge(no_chrome: true), fcb)
1087
- end, opts
1088
- )
1089
- end
1090
-
1091
- # return true if values were modified
1092
- # execute block once per filename
1093
- #
1094
- def load_auto_blocks(opts, blocks_in_file)
1095
- return unless opts[:document_load_opts_block_name].present?
1096
- return if opts[:s_most_recent_filename] == opts[:filename]
1097
-
1098
- block = block_find(blocks_in_file, :oname,
1099
- opts[:document_load_opts_block_name])
1100
- return unless block
1101
-
1102
- handle_shell_opts(opts, block, @options)
1103
- opts[:s_most_recent_filename] = opts[:filename]
1104
- true
1105
- end
1106
-
1107
- def mdoc_and_menu_from_file(opts)
1108
- menu_blocks = menu_from_file(opts.merge(struct: true))
1109
- mdoc = MDoc.new(menu_blocks) do |nopts|
1110
- opts.merge!(nopts)
1111
- end
1112
- [menu_blocks, mdoc]
1113
- end
1114
-
1115
- ## Handles the file loading and returns the blocks in the file and MDoc instance
1116
- #
1117
- def mdoc_menu_and_selected_from_file(opts)
1118
- blocks_in_file, mdoc = mdoc_and_menu_from_file(opts)
1119
- if load_auto_blocks(opts, blocks_in_file)
1120
- # recreate menu with new options
1121
- #
1122
- blocks_in_file, mdoc = mdoc_and_menu_from_file(opts)
1123
- end
1124
-
1125
- blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
1126
- add_menu_chrome_blocks!(blocks_menu)
1127
- [blocks_in_file, blocks_menu, mdoc]
1128
- end
1129
-
1130
- def menu_chrome_colored_option(opts,
1131
- option_symbol = :menu_option_back_name)
1132
- if opts[:menu_chrome_color]
1133
- menu_chrome_formatted_option(opts,
1134
- option_symbol).send(opts[:menu_chrome_color].to_sym)
1135
- else
1136
- menu_chrome_formatted_option(opts, option_symbol)
1137
- end
1138
- end
1139
-
1140
- def menu_chrome_formatted_option(opts,
1141
- option_symbol = :menu_option_back_name)
1142
- val1 = safeval(opts.fetch(option_symbol, ''))
1143
- val1 unless opts[:menu_chrome_format]
1144
-
1145
- format(opts[:menu_chrome_format], val1)
1146
- end
1147
-
1148
- def menu_export(data = menu_for_optparse)
1149
- data.map do |item|
1150
- item.delete(:procname)
1151
- item
1152
- end.to_yaml
1153
- end
433
+ private
1154
434
 
1155
435
  ##
1156
436
  # Generates a menu suitable for OptionParser from the menu items defined in YAML format.
@@ -1164,37 +444,6 @@ module MarkdownExec
1164
444
  end
1165
445
  end
1166
446
 
1167
- ##
1168
- # Returns a list of blocks in a given file, including dividers, tasks, and other types of blocks.
1169
- # The list can be customized via call_options and options_block.
1170
- #
1171
- # @param call_options [Hash] Options passed as an argument.
1172
- # @param options_block [Proc] Block for dynamic option manipulation.
1173
- # @return [Array<FCB>] An array of FCB objects representing the blocks.
1174
- #
1175
- def menu_from_file(call_options = {},
1176
- &options_block)
1177
- opts = optsmerge(call_options, options_block)
1178
- use_chrome = !opts[:no_chrome]
1179
-
1180
- blocks = []
1181
- iter_blocks_in_file(opts) do |btype, fcb|
1182
- case btype
1183
- when :blocks
1184
- append_block_summary(blocks, fcb, opts)
1185
- when :filter # what btypes are responded to?
1186
- %i[blocks line]
1187
- when :line
1188
- create_and_add_chrome_blocks(blocks, fcb, opts, use_chrome)
1189
- end
1190
- end
1191
- blocks
1192
- rescue StandardError => err
1193
- warn(error = "ERROR ** MarkParse.menu_from_file(); #{err.inspect}")
1194
- warn(caller[0..4])
1195
- raise StandardError, error
1196
- end
1197
-
1198
447
  def menu_help
1199
448
  @option_parser.help
1200
449
  end
@@ -1203,10 +452,26 @@ module MarkdownExec
1203
452
  data.map(&block)
1204
453
  end
1205
454
 
1206
- def menu_option_append(opts, options, item)
1207
- unless item[:long_name].present? || item[:short_name].present?
1208
- return
1209
- end
455
+ def opts_list_files(options)
456
+ list_files_specified(
457
+ determine_filename(
458
+ specified_filename: options[:filename]&.present? ? options[:filename] : nil,
459
+ specified_folder: options[:path],
460
+ default_filename: 'README.md',
461
+ default_folder: '.'
462
+ )
463
+ )
464
+ end
465
+
466
+ def menu_export(data = menu_for_optparse)
467
+ data.map do |item|
468
+ item.delete(:procname)
469
+ item
470
+ end.to_yaml
471
+ end
472
+
473
+ def opts_menu_option_append(opts, options, item)
474
+ return unless item[:long_name].present? || item[:short_name].present?
1210
475
 
1211
476
  opts.on(*[
1212
477
  # - long name
@@ -1234,188 +499,9 @@ module MarkdownExec
1234
499
  ].compact)
1235
500
  end
1236
501
 
1237
- def menu_with_block_labels(call_options = {})
1238
- opts = options.merge(call_options)
1239
- menu_from_file(opts).map do |fcb|
1240
- BlockLabel.make(
1241
- filename: opts[:filename],
1242
- headings: fcb.fetch(:headings, []),
1243
- menu_blocks_with_docname: opts[:menu_blocks_with_docname],
1244
- menu_blocks_with_headings: opts[:menu_blocks_with_headings],
1245
- title: fcb[:title],
1246
- text: fcb[:text],
1247
- body: fcb[:body]
1248
- )
1249
- end.compact
1250
- end
1251
-
1252
- def next_block_name_from_command_line_arguments(opts)
1253
- if opts[:s_cli_rest].present?
1254
- opts[:block_name] = opts[:s_cli_rest].pop
1255
- false # repeat_menu
1256
- else
1257
- true # repeat_menu
1258
- end
1259
- end
1260
-
1261
- # :reek:ControlParameter
1262
- def optsmerge(call_options = {}, options_block = nil)
1263
- class_call_options = @options.merge(call_options || {})
1264
- if options_block
1265
- options_block.call class_call_options
1266
- else
1267
- class_call_options
1268
- end
1269
- end
1270
-
1271
- def output_execution_result
1272
- oq = [['Block', @options[:block_name], DISPLAY_LEVEL_ADMIN],
1273
- ['Command',
1274
- [MarkdownExec::BIN_NAME,
1275
- @options[:filename],
1276
- @options[:block_name]].join(' '),
1277
- DISPLAY_LEVEL_ADMIN]]
1278
-
1279
- [['Script', :saved_filespec],
1280
- ['StdOut', :logged_stdout_filespec]].each do |label, name|
1281
- if @options[name]
1282
- oq << [label, @options[name],
1283
- DISPLAY_LEVEL_ADMIN]
1284
- end
1285
- end
1286
-
1287
- oq.map do |label, value, level|
1288
- lout ["#{label}:".yellow, value.to_s].join(' '), level: level
1289
- end
1290
- end
1291
-
1292
- def output_execution_summary
1293
- return unless @options[:output_execution_summary]
1294
-
1295
- fout_section 'summary', {
1296
- execute_aborted_at: @execute_aborted_at,
1297
- execute_completed_at: @execute_completed_at,
1298
- execute_error: @execute_error,
1299
- execute_error_message: @execute_error_message,
1300
- execute_files: @execute_files,
1301
- execute_options: @execute_options,
1302
- execute_started_at: @execute_started_at,
1303
- execute_script_filespec: @execute_script_filespec
1304
- }
1305
- end
1306
-
1307
- # Prepare the blocks menu by adding labels and other necessary details.
1308
- #
1309
- # @param blocks_in_file [Array<Hash>] The list of blocks from the file.
1310
- # @param opts [Hash] The options hash.
1311
- # @return [Array<Hash>] The updated blocks menu.
1312
- def prepare_blocks_menu(blocks_in_file, opts)
1313
- # next if fcb.fetch(:disabled, false)
1314
- # next unless fcb.fetch(:name, '').present?
1315
- replace_consecutive_blanks(blocks_in_file).map do |fcb|
1316
- next if Filter.prepared_not_in_menu?(opts, fcb)
1317
-
1318
- fcb.merge!(
1319
- name: indent_all_lines(fcb.dname, fcb.fetch(:indent, nil)),
1320
- label: BlockLabel.make(
1321
- body: fcb[:body],
1322
- filename: opts[:filename],
1323
- headings: fcb.fetch(:headings, []),
1324
- menu_blocks_with_docname: opts[:menu_blocks_with_docname],
1325
- menu_blocks_with_headings: opts[:menu_blocks_with_headings],
1326
- text: fcb[:text],
1327
- title: fcb[:title]
1328
- )
1329
- )
1330
- fcb.to_h
1331
- end.compact
1332
- end
1333
-
1334
502
  # Prepares and fetches file listings
1335
- def prepare_file_list(options)
1336
- list_files_per_options(options)
1337
- end
1338
-
1339
- def process_fenced_block(fcb, opts, selected_messages, &block)
1340
- fcb.oname = fcb.dname = fcb.title || ''
1341
- return unless fcb.body
1342
-
1343
- update_title_from_body(fcb)
1344
-
1345
- if block &&
1346
- selected_messages.include?(:blocks) &&
1347
- Filter.fcb_select?(opts, fcb)
1348
- block.call :blocks, fcb
1349
- end
1350
- end
1351
-
1352
- def process_line(line, _opts, selected_messages, &block)
1353
- return unless block && selected_messages.include?(:line)
1354
-
1355
- # dp 'text outside of fcb'
1356
- fcb = FCB.new
1357
- fcb.body = [line]
1358
- block.call(:line, fcb)
1359
- end
1360
-
1361
- class MenuOptions
1362
- YES = 1
1363
- NO = 2
1364
- SCRIPT_TO_CLIPBOARD = 3
1365
- SAVE_SCRIPT = 4
1366
- end
1367
-
1368
- ##
1369
- # Presents a menu to the user for approving an action and performs additional tasks based on the selection.
1370
- # The function provides options for approval, rejection, copying data to clipboard, or saving data to a file.
1371
- #
1372
- # @param opts [Hash] A hash containing various options for the menu.
1373
- # @param required_lines [Array<String>] Lines of text or code that are subject to user approval.
1374
- #
1375
- # @option opts [String] :prompt_approve_block Prompt text for the approval menu.
1376
- # @option opts [String] :prompt_yes Text for the 'Yes' choice in the menu.
1377
- # @option opts [String] :prompt_no Text for the 'No' choice in the menu.
1378
- # @option opts [String] :prompt_script_to_clipboard Text for the 'Copy to Clipboard' choice in the menu.
1379
- # @option opts [String] :prompt_save_script Text for the 'Save to File' choice in the menu.
1380
- #
1381
- # @return [Boolean] Returns true if the user approves (selects 'Yes'), false otherwise.
1382
- ##
1383
- def prompt_for_user_approval(opts, required_lines)
1384
- # Present a selection menu for user approval.
1385
-
1386
- sel = @prompt.select(opts[:prompt_approve_block],
1387
- filter: true) do |menu|
1388
- menu.default MenuOptions::YES
1389
- menu.choice opts[:prompt_yes], MenuOptions::YES
1390
- menu.choice opts[:prompt_no], MenuOptions::NO
1391
- menu.choice opts[:prompt_script_to_clipboard],
1392
- MenuOptions::SCRIPT_TO_CLIPBOARD
1393
- menu.choice opts[:prompt_save_script], MenuOptions::SAVE_SCRIPT
1394
- end
1395
-
1396
- if sel == MenuOptions::SCRIPT_TO_CLIPBOARD
1397
- copy_to_clipboard(required_lines)
1398
- elsif sel == MenuOptions::SAVE_SCRIPT
1399
- save_to_file(opts, required_lines)
1400
- end
1401
-
1402
- sel == MenuOptions::YES
1403
- rescue TTY::Reader::InputInterrupt
1404
- exit 1
1405
- end
1406
-
1407
- def prompt_select_continue(opts)
1408
- sel = @prompt.select(
1409
- opts[:prompt_after_bash_exec],
1410
- filter: true,
1411
- quiet: true
1412
- ) do |menu|
1413
- menu.choice opts[:prompt_yes]
1414
- menu.choice opts[:prompt_exit]
1415
- end
1416
- sel == opts[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
1417
- rescue TTY::Reader::InputInterrupt
1418
- exit 1
503
+ def opts_prepare_file_list(options)
504
+ opts_list_files(options)
1419
505
  end
1420
506
 
1421
507
  # :reek:UtilityFunction ### temp
@@ -1426,53 +512,18 @@ module MarkdownExec
1426
512
  .transform_keys(&:to_sym))
1427
513
  end
1428
514
 
1429
- # Reads required code blocks from a temporary file specified by an environment variable.
1430
- #
1431
- # @return [Array<String>] An array containing the lines read from the temporary file.
1432
- # @note Relies on the 'MDE_LINK_REQUIRED_FILE' environment variable to locate the file.
1433
- def read_required_blocks_from_temp_file
1434
- temp_blocks = []
1435
-
1436
- temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
1437
- if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
1438
- return temp_blocks
1439
- end
1440
-
1441
- if File.exist?(temp_blocks_file_path)
1442
- temp_blocks = File.readlines(temp_blocks_file_path, chomp: true)
1443
- end
1444
-
1445
- temp_blocks
1446
- end
1447
-
1448
- # Replace duplicate blanks (where :oname is not present) with a single blank line.
1449
- #
1450
- # @param [Array<Hash>] lines Array of hashes to process.
1451
- # @return [Array<Hash>] Cleaned array with consecutive blanks collapsed into one.
1452
- def replace_consecutive_blanks(lines)
1453
- lines.chunk_while do |i, j|
1454
- i[:oname].to_s.empty? && j[:oname].to_s.empty?
1455
- end.map do |chunk|
1456
- if chunk.any? do |line|
1457
- line[:oname].to_s.strip.empty?
1458
- end
1459
- chunk.first
1460
- else
1461
- chunk
1462
- end
1463
- end.flatten
1464
- end
515
+ public
1465
516
 
1466
517
  def run
1467
518
  clear_required_file
1468
519
  execute_block_with_error_handling(initialize_and_parse_cli_options)
1469
- delete_required_temp_file
1470
- rescue StandardError => err
1471
- warn(error = "ERROR ** MarkParse.run(); #{err.inspect}")
1472
- binding.pry if $tap_enable
1473
- raise ArgumentError, error
520
+ @options.delete_required_temp_file
521
+ rescue StandardError
522
+ error_handler('run')
1474
523
  end
1475
524
 
525
+ private
526
+
1476
527
  def run_last_script
1477
528
  filename = SavedFilesMatcher.most_recent(@options[:saved_script_folder],
1478
529
  @options[:saved_script_glob])
@@ -1480,89 +531,20 @@ module MarkdownExec
1480
531
 
1481
532
  saved_name_split filename
1482
533
  @options[:save_executed_script] = false
1483
- select_approve_and_execute_block
1484
- end
1485
-
1486
- def safeval(str)
1487
- eval(str)
534
+ @options.select_approve_and_execute_block
1488
535
  rescue StandardError
1489
- warn $!
1490
- binding.pry if $tap_enable
1491
- raise StandardError, $!
1492
- end
1493
-
1494
- def save_to_file(opts, required_lines)
1495
- write_command_file(opts.merge(save_executed_script: true),
1496
- required_lines)
1497
- fout "File saved: #{@options[:saved_filespec]}"
536
+ error_handler('run_last_script')
1498
537
  end
1499
538
 
1500
539
  def saved_name_split(name)
1501
540
  # rubocop:disable Layout/LineLength
1502
- mf = /#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_,_(?<block>.+)\.sh/.match name
541
+ mf = /#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_,_(?<block>.+)\.sh/.match(name)
1503
542
  # rubocop:enable Layout/LineLength
1504
543
  return unless mf
1505
544
 
1506
545
  @options[:block_name] = mf[:block]
1507
- @options[:filename] = mf[:file].gsub(FNR12, FNR11)
1508
- end
1509
-
1510
- # Select and execute a code block from a Markdown document.
1511
- #
1512
- # This method allows the user to interactively select a code block from a
1513
- # Markdown document, obtain approval, and execute the chosen block of code.
1514
- #
1515
- # @param call_options [Hash] Initial options for the method.
1516
- # @param options_block [Block] Block of options to be merged with call_options.
1517
- # @return [Nil] Returns nil if no code block is selected or an error occurs.
1518
- def select_approve_and_execute_block(call_options = {},
1519
- &options_block)
1520
- base_opts = optsmerge(call_options, options_block)
1521
- repeat_menu = true && !base_opts[:block_name].present?
1522
- load_file = LoadFile::Reuse
1523
- default = nil
1524
- block = nil
1525
-
1526
- loop do
1527
- loop do
1528
- opts = base_opts.dup
1529
- opts[:s_back] = false
1530
- blocks_in_file, blocks_menu, mdoc = mdoc_menu_and_selected_from_file(opts)
1531
- block, state = command_or_user_selected_block(blocks_in_file, blocks_menu,
1532
- default, opts)
1533
- return if state == MenuState::EXIT
1534
-
1535
- load_file, next_block_name = approve_and_execute_block(block, opts,
1536
- mdoc)
1537
- default = load_file == LoadFile::Load ? nil : opts[:block_name]
1538
- base_opts[:block_name] = opts[:block_name] = next_block_name
1539
- base_opts[:filename] = opts[:filename]
1540
-
1541
- # user prompt to exit if the menu will be displayed again
1542
- #
1543
- if repeat_menu &&
1544
- block[:shell] == BlockType::BASH &&
1545
- opts[:pause_after_bash_exec] &&
1546
- prompt_select_continue(opts) == MenuState::EXIT
1547
- return
1548
- end
1549
-
1550
- # exit current document/menu if loading next document or single block_name was specified
1551
- #
1552
- if state == MenuState::CONTINUE && load_file == LoadFile::Load
1553
- break
1554
- end
1555
- break unless repeat_menu
1556
- end
1557
- break if load_file == LoadFile::Reuse
1558
-
1559
- repeat_menu = next_block_name_from_command_line_arguments(base_opts)
1560
- end
1561
- rescue StandardError => err
1562
- warn(error = "ERROR ** MarkParse.select_approve_and_execute_block(); #{err.inspect}")
1563
- warn err.backtrace
1564
- binding.pry if $tap_enable
1565
- raise ArgumentError, error
546
+ @options[:filename] = mf[:file].gsub(@options[:saved_filename_pattern],
547
+ @options[:saved_filename_replacement])
1566
548
  end
1567
549
 
1568
550
  def select_document_if_multiple(files = list_markdown_files_in_path)
@@ -1571,44 +553,24 @@ module MarkdownExec
1571
553
  return unless count >= 2
1572
554
 
1573
555
  opts = options.dup
1574
- select_option_or_exit opts[:prompt_select_md].to_s, files,
1575
- opts.merge(per_page: opts[:select_page_height])
556
+ select_option_or_exit(HashDelegator.new(@options).string_send_color(opts[:prompt_select_md].to_s, :prompt_color_after_script_execution),
557
+ files,
558
+ opts.merge(per_page: opts[:select_page_height]))
1576
559
  end
1577
560
 
1578
561
  # Presents a TTY prompt to select an option or exit, returns selected option or nil
1579
- def select_option_or_exit(prompt_text, items, opts = {})
1580
- result = select_option_with_metadata(prompt_text, items, opts)
562
+ def select_option_or_exit(prompt_text, strings, opts = {})
563
+ result = @options.select_option_with_metadata(prompt_text, strings,
564
+ opts)
1581
565
  return unless result.fetch(:option, nil)
1582
566
 
1583
567
  result[:selected]
1584
568
  end
1585
569
 
1586
- # Presents a TTY prompt to select an option or exit, returns metadata including option and selected
1587
- def select_option_with_metadata(prompt_text, items, opts = {})
1588
- selection = @prompt.select(prompt_text,
1589
- items,
1590
- opts.merge(filter: true))
1591
-
1592
- items.find { |item| item[:dname] == selection }
1593
- .merge(
1594
- if selection == menu_chrome_colored_option(opts,
1595
- :menu_option_back_name)
1596
- { option: selection, curr: @hs_curr, rest: @hs_rest,
1597
- shell: BlockType::LINK }
1598
- elsif selection == menu_chrome_colored_option(opts,
1599
- :menu_option_exit_name)
1600
- { option: selection }
1601
- else
1602
- { selected: selection }
1603
- end
1604
- )
1605
- rescue TTY::Reader::InputInterrupt
1606
- exit 1
1607
- end
1608
-
1609
570
  def select_recent_output
1610
571
  filename = select_option_or_exit(
1611
- @options[:prompt_select_output].to_s,
572
+ HashDelegator.new(@options).string_send_color(@options[:prompt_select_output].to_s,
573
+ :prompt_color_after_script_execution),
1612
574
  list_recent_output(
1613
575
  @options[:saved_stdout_folder],
1614
576
  @options[:saved_stdout_glob],
@@ -1623,7 +585,8 @@ module MarkdownExec
1623
585
 
1624
586
  def select_recent_script
1625
587
  filename = select_option_or_exit(
1626
- @options[:prompt_select_md].to_s,
588
+ HashDelegator.new(@options).string_send_color(@options[:prompt_select_md].to_s,
589
+ :prompt_color_after_script_execution),
1627
590
  list_recent_scripts(
1628
591
  @options[:saved_script_folder],
1629
592
  @options[:saved_script_glob],
@@ -1635,38 +598,10 @@ module MarkdownExec
1635
598
 
1636
599
  saved_name_split(filename)
1637
600
 
1638
- select_approve_and_execute_block({ bash: true,
1639
- save_executed_script: false,
1640
- struct: true })
601
+ @options.select_approve_and_execute_block ### ({ save_executed_script: false })
1641
602
  end
1642
603
 
1643
- def start_fenced_block(opts, line, headings,
1644
- fenced_start_extended_regex)
1645
- fcb_title_groups = line.match(fenced_start_extended_regex).named_captures.sym_keys
1646
- rest = fcb_title_groups.fetch(:rest, '')
1647
-
1648
- fcb = FCB.new
1649
- fcb.headings = headings
1650
- fcb.oname = fcb.dname = fcb_title_groups.fetch(:name, '')
1651
- fcb.indent = fcb_title_groups.fetch(:indent, '')
1652
- fcb.shell = fcb_title_groups.fetch(:shell, '')
1653
- fcb.title = fcb_title_groups.fetch(:name, '')
1654
- fcb.body = []
1655
- fcb.reqs, fcb.wraps =
1656
- ArrayUtil.partition_by_predicate(rest.scan(/\+[^\s]+/).map do |req|
1657
- req[1..-1]
1658
- end) do |name|
1659
- !name.match(Regexp.new(opts[:block_name_wrapper_match]))
1660
- end
1661
- fcb.call = rest.match(Regexp.new(opts[:block_calls_scan]))&.to_a&.first
1662
- fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
1663
- tn.named_captures.sym_keys
1664
- end
1665
- fcb.stdout = if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
1666
- tn.named_captures.sym_keys
1667
- end
1668
- fcb
1669
- end
604
+ public
1670
605
 
1671
606
  def tab_completions(data = menu_for_optparse)
1672
607
  data.map do |item|
@@ -1674,89 +609,6 @@ module MarkdownExec
1674
609
  end.compact
1675
610
  end
1676
611
 
1677
- def tty_prompt_without_disabled_symbol
1678
- TTY::Prompt.new(interrupt: lambda {
1679
- puts;
1680
- raise TTY::Reader::InputInterrupt
1681
- },
1682
- symbols: { cross: ' ' })
1683
- end
1684
-
1685
- ##
1686
- # Updates the hierarchy of document headings based on the given line and existing headings.
1687
- # The function uses regular expressions specified in the `opts` to identify different levels of headings.
1688
- #
1689
- # @param line [String] The line of text to examine for heading content.
1690
- # @param headings [Array<String>] The existing list of document headings.
1691
- # @param opts [Hash] A hash containing options for regular expression matches for different heading levels.
1692
- #
1693
- # @option opts [String] :heading1_match Regular expression for matching first-level headings.
1694
- # @option opts [String] :heading2_match Regular expression for matching second-level headings.
1695
- # @option opts [String] :heading3_match Regular expression for matching third-level headings.
1696
- #
1697
- # @return [Array<String>] Updated list of headings.
1698
- def update_document_headings(line, headings, opts)
1699
- if (lm = line.match(Regexp.new(opts[:heading3_match])))
1700
- [headings[0], headings[1], lm[:name]]
1701
- elsif (lm = line.match(Regexp.new(opts[:heading2_match])))
1702
- [headings[0], lm[:name]]
1703
- elsif (lm = line.match(Regexp.new(opts[:heading1_match])))
1704
- [lm[:name]]
1705
- else
1706
- headings
1707
- end
1708
- end
1709
-
1710
- ##
1711
- # Processes an individual line within a loop, updating headings and handling fenced code blocks.
1712
- # This function is designed to be called within a loop that iterates through each line of a document.
1713
- #
1714
- # @param line [String] The current line being processed.
1715
- # @param state [Hash] The current state of the parser, including flags and data related to the processing.
1716
- # @param opts [Hash] A hash containing various options for line and block processing.
1717
- # @param selected_messages [Array<String>] Accumulator for lines or messages that are subject to further processing.
1718
- # @param block [Proc] An optional block for further processing or transformation of lines.
1719
- #
1720
- # @option state [Array<String>] :headings Current headings to be updated based on the line.
1721
- # @option state [Regexp] :fenced_start_and_end_regex Regular expression to match the start and end of a fenced block.
1722
- # @option state [Boolean] :in_fenced_block Flag indicating whether the current line is inside a fenced block.
1723
- # @option state [Object] :fcb An object representing the current fenced code block being processed.
1724
- #
1725
- # @option opts [Boolean] :menu_blocks_with_headings Flag indicating whether to update headings while processing.
1726
- #
1727
- # @return [Void] The function modifies the `state` and `selected_messages` arguments in place.
1728
- ##
1729
- def update_line_and_block_state(line, state, opts, selected_messages,
1730
- &block)
1731
- if opts[:menu_blocks_with_headings]
1732
- state[:headings] =
1733
- update_document_headings(line, state[:headings], opts)
1734
- end
1735
-
1736
- if line.match(state[:fenced_start_and_end_regex])
1737
- if state[:in_fenced_block]
1738
- process_fenced_block(state[:fcb], opts, selected_messages,
1739
- &block)
1740
- state[:in_fenced_block] = false
1741
- else
1742
- state[:fcb] =
1743
- start_fenced_block(opts, line, state[:headings],
1744
- state[:fenced_start_extended_regex])
1745
- state[:in_fenced_block] = true
1746
- end
1747
- elsif state[:in_fenced_block] && state[:fcb].body
1748
- ## add line to fenced code block
1749
- # remove fcb indent if possible
1750
- #
1751
- state[:fcb].body += [
1752
- line.chomp.sub(/^#{state[:fcb].indent}/, '')
1753
- ]
1754
-
1755
- else
1756
- process_line(line, opts, selected_messages, &block)
1757
- end
1758
- end
1759
-
1760
612
  # :reek:BooleanParameter
1761
613
  # :reek:ControlParameter
1762
614
  def update_options(opts = {}, over: true)
@@ -1767,125 +619,6 @@ module MarkdownExec
1767
619
  end
1768
620
  @options
1769
621
  end
1770
-
1771
- # Updates the title of an FCB object from its body content if the title is nil or empty.
1772
- def update_title_from_body(fcb)
1773
- return unless fcb.title.nil? || fcb.title.empty?
1774
-
1775
- fcb.title = derive_title_from_body(fcb)
1776
- end
1777
-
1778
- def wait_for_user_selected_block(blocks_in_file, blocks_menu,
1779
- default, opts)
1780
- block, state = wait_for_user_selection(blocks_in_file, blocks_menu,
1781
- default, opts)
1782
- case state
1783
- when MenuState::BACK
1784
- opts[:block_name] = block[:dname]
1785
- opts[:s_back] = true
1786
- when MenuState::CONTINUE
1787
- opts[:block_name] = block[:dname]
1788
- end
1789
-
1790
- [block, state]
1791
- end
1792
-
1793
- ## Handles the menu interaction and returns selected block and option state
1794
- #
1795
- def wait_for_user_selection(blocks_in_file, blocks_menu, default,
1796
- opts)
1797
- pt = opts[:prompt_select_block].to_s
1798
- bm = prepare_blocks_menu(blocks_menu, opts)
1799
- return [nil, MenuState::EXIT] if bm.count.zero?
1800
-
1801
- o2 = if default
1802
- opts.merge(default: default)
1803
- else
1804
- opts
1805
- end
1806
-
1807
- obj = select_option_with_metadata(pt, bm, o2.merge(
1808
- per_page: opts[:select_page_height]
1809
- ))
1810
-
1811
- case obj.fetch(:oname, nil)
1812
- when menu_chrome_formatted_option(opts, :menu_option_exit_name)
1813
- [nil, MenuState::EXIT]
1814
- when menu_chrome_formatted_option(opts, :menu_option_back_name)
1815
- [obj, MenuState::BACK]
1816
- else
1817
- [obj, MenuState::CONTINUE]
1818
- end
1819
- rescue StandardError => err
1820
- warn(error = "ERROR ** MarkParse.wait_for_user_selection(); #{err.inspect}")
1821
- warn caller.take(3)
1822
- binding.pry if $tap_enable
1823
- raise ArgumentError, error
1824
- end
1825
-
1826
- # Handles the core logic for generating the command file's metadata and content.
1827
- def write_command_file(call_options, required_lines)
1828
- return unless call_options[:save_executed_script]
1829
-
1830
- time_now = Time.now.utc
1831
- opts = optsmerge call_options
1832
- opts[:saved_script_filename] =
1833
- SavedAsset.script_name(blockname: opts[:block_name],
1834
- filename: opts[:filename],
1835
- prefix: opts[:saved_script_filename_prefix],
1836
- time: time_now)
1837
-
1838
- @execute_script_filespec =
1839
- @options[:saved_filespec] =
1840
- File.join opts[:saved_script_folder],
1841
- opts[:saved_script_filename]
1842
-
1843
- shebang = if @options[:shebang]&.present?
1844
- "#{@options[:shebang]} #{@options[:shell]}\n"
1845
- else
1846
- ''
1847
- end
1848
-
1849
- content = shebang +
1850
- "# file_name: #{opts[:filename]}\n" \
1851
- "# block_name: #{opts[:block_name]}\n" \
1852
- "# time: #{time_now}\n" \
1853
- "#{required_lines.flatten.join("\n")}\n"
1854
-
1855
- create_and_write_file_with_permissions(@options[:saved_filespec], content,
1856
- @options[:saved_script_chmod])
1857
- end
1858
-
1859
- def write_execution_output_to_file
1860
- FileUtils.mkdir_p File.dirname(@options[:logged_stdout_filespec])
1861
-
1862
- ol = ["-STDOUT-\n"]
1863
- ol += @execute_files&.fetch(EF_STDOUT, [])
1864
- ol += ["\n-STDERR-\n"]
1865
- ol += @execute_files&.fetch(EF_STDERR, [])
1866
- ol += ["\n-STDIN-\n"]
1867
- ol += @execute_files&.fetch(EF_STDIN, [])
1868
- ol += ["\n"]
1869
- File.write(@options[:logged_stdout_filespec], ol.join)
1870
- end
1871
-
1872
- # Writes required code blocks to a temporary file and sets an environment variable with its path.
1873
- #
1874
- # @param block_name [String] The name of the block to collect code for.
1875
- # @param opts [Hash] Additional options for collecting code.
1876
- # @note Sets the 'MDE_LINK_REQUIRED_FILE' environment variable to the temporary file path.
1877
- def write_required_blocks_to_temp_file(mdoc, block_name, opts = {})
1878
- code_blocks = (read_required_blocks_from_temp_file +
1879
- mdoc.collect_recursively_required_code(
1880
- block_name,
1881
- opts: opts
1882
- )[:code]).join("\n")
1883
-
1884
- Dir::Tmpname.create(self.class.to_s) do |path|
1885
- File.write(path, code_blocks)
1886
- ENV['MDE_LINK_REQUIRED_FILE'] = path
1887
- end
1888
- end
1889
622
  end # class MarkParse
1890
623
  end # module MarkdownExec
1891
624
 
@@ -1896,60 +629,6 @@ if $PROGRAM_NAME == __FILE__
1896
629
  require 'minitest/autorun'
1897
630
 
1898
631
  module MarkdownExec
1899
- class TestMarkParse < Minitest::Test
1900
- require 'mocha/minitest'
1901
-
1902
- def test_calling_execute_approved_block_calls_command_execute_with_argument_args_value
1903
- pigeon = 'E'
1904
- obj = { s_pass_args: pigeon }
1905
-
1906
- c = MarkdownExec::MarkParse.new
1907
-
1908
- # Expect that method command_execute is called with argument args having value pigeon
1909
- c.expects(:command_execute).with(
1910
- obj,
1911
- '',
1912
- args: pigeon
1913
- )
1914
-
1915
- # Call method execute_approved_block
1916
- c.execute_approved_block(obj, [])
1917
- end
1918
-
1919
- def setup
1920
- @mark_parse = MarkdownExec::MarkParse.new
1921
- end
1922
-
1923
- def test_set_fcb_title
1924
- # sample input and output data for testing update_title_from_body method
1925
- input_output_data = [
1926
- {
1927
- input: FCB.new(title: nil, body: ["puts 'Hello, world!'"]),
1928
- output: "puts 'Hello, world!'"
1929
- },
1930
- {
1931
- input: FCB.new(title: '',
1932
- body: ['def add(x, y)',
1933
- ' x + y', 'end']),
1934
- output: "def add(x, y)\n x + y\n end\n"
1935
- },
1936
- {
1937
- input: FCB.new(title: 'foo', body: %w[bar baz]),
1938
- output: 'foo' # expect the title to remain unchanged
1939
- }
1940
- ]
1941
-
1942
- # iterate over the input and output data and
1943
- # assert that the method sets the title as expected
1944
- input_output_data.each do |data|
1945
- input = data[:input]
1946
- output = data[:output]
1947
- @mark_parse.update_title_from_body(input)
1948
- assert_equal output, input.title
1949
- end
1950
- end
1951
- end
1952
-
1953
632
  def test_select_block
1954
633
  blocks = [block1, block2]
1955
634
  menu = [m1, m2]