markdown_exec 1.6 → 1.7

Sign up to get free protection for your applications and to get access to all the features.
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]