markdown_exec 1.5 → 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,240 +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
- if opts[:menu_note_match].present? && (mbody = fcb.body[0].match opts[:menu_note_match])
542
- create_and_add_chrome_block(blocks, fcb, mbody, opts[:menu_note_format],
543
- opts[:menu_note_color].to_sym)
544
- elsif opts[:menu_divider_match].present? && (mbody = fcb.body[0].match opts[:menu_divider_match])
545
- create_and_add_chrome_block(blocks, fcb, mbody, opts[:menu_divider_format],
546
- opts[:menu_divider_color].to_sym)
547
- elsif opts[:menu_task_match].present? && (mbody = fcb.body[0].match opts[:menu_task_match])
548
- create_and_add_chrome_block(blocks, fcb, mbody, opts[:menu_task_format],
549
- opts[:menu_task_color].to_sym)
550
- end
551
- end
552
-
553
- def create_and_write_file_with_permissions(file_path, content,
554
- chmod_value)
555
- dirname = File.dirname(file_path)
556
- FileUtils.mkdir_p dirname
557
- File.write(file_path, content)
558
- return if chmod_value.zero?
559
-
560
- File.chmod chmod_value, file_path
561
- end
562
-
563
- # Deletes a required temporary file specified by an environment variable.
564
- # The function checks if the file exists before attempting to delete it.
565
- # Clears the environment variable after deletion.
566
- #
567
- def delete_required_temp_file
568
- temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
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)
569
183
 
570
- if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
571
- return
572
- end
184
+ # return if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
573
185
 
574
- FileUtils.rm_f(temp_blocks_file_path)
186
+ # FileUtils.rm_f(temp_blocks_file_path)
575
187
 
576
- clear_required_file
577
- end
188
+ # clear_required_file
189
+ # rescue StandardError
190
+ # error_handler('delete_required_temp_file')
191
+ # end
578
192
 
579
- # Derives a title from the body of an FCB object.
580
- # @param fcb [Object] The FCB object whose title is to be derived.
581
- # @return [String] The derived title.
582
- def derive_title_from_body(fcb)
583
- body_content = fcb&.body
584
- return '' unless body_content
585
-
586
- if body_content.count == 1
587
- body_content.first
588
- else
589
- format_multiline_body_as_title(body_content)
590
- end
591
- end
193
+ public
592
194
 
593
195
  ## Determines the correct filename to use for searching files
594
196
  #
@@ -597,8 +199,7 @@ module MarkdownExec
597
199
  if specified_filename&.present?
598
200
  return specified_filename if specified_filename.start_with?('/')
599
201
 
600
- File.join(specified_folder || default_folder,
601
- specified_filename)
202
+ File.join(specified_folder || default_folder, specified_filename)
602
203
  elsif specified_folder&.present?
603
204
  File.join(specified_folder,
604
205
  filetree ? @options[:md_filename_match] : @options[:md_filename_glob])
@@ -607,31 +208,19 @@ module MarkdownExec
607
208
  end
608
209
  end
609
210
 
610
- # :reek:DuplicateMethodCall
611
- def display_required_code(opts, required_lines)
612
- frame = opts[:output_divider].send(opts[:output_divider_color].to_sym)
613
- fout frame
614
- required_lines.each { |cb| fout cb }
615
- fout frame
616
- end
211
+ private
617
212
 
618
- def execute_approved_block(opts, required_lines)
619
- write_command_file(opts, required_lines)
620
- command_execute(
621
- opts,
622
- required_lines.flatten.join("\n"),
623
- args: opts.fetch(:s_pass_args, [])
624
- )
625
- initialize_and_save_execution_output
626
- output_execution_summary
627
- output_execution_result
628
- 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
629
219
 
630
220
  # Reports and executes block logic
631
221
  def execute_block_logic(files)
632
222
  @options[:filename] = select_document_if_multiple(files)
633
- select_approve_and_execute_block({ bash: true,
634
- struct: true })
223
+ @options.select_approve_and_execute_block
635
224
  end
636
225
 
637
226
  ## Executes the block specified in the options
@@ -640,12 +229,10 @@ module MarkdownExec
640
229
  finalize_cli_argument_processing(rest)
641
230
  @options[:s_cli_rest] = rest
642
231
  execute_code_block_based_on_options(@options)
643
- rescue FileMissingError => err
644
- puts "File missing: #{err}"
645
- rescue StandardError => err
646
- warn(error = "ERROR ** MarkParse.execute_block_with_error_handling(); #{err.inspect}")
647
- binding.pry if $tap_enable
648
- raise ArgumentError, error
232
+ rescue FileMissingError
233
+ warn "File missing: #{$!}"
234
+ rescue StandardError
235
+ error_handler('execute_block_with_error_handling')
649
236
  end
650
237
 
651
238
  # Main method to execute a block based on options and block_name
@@ -654,47 +241,40 @@ module MarkdownExec
654
241
  update_options(options, over: false)
655
242
 
656
243
  simple_commands = {
657
- doc_glob: -> { fout options[:md_filename_glob] },
658
- list_blocks: lambda do
659
- fout_list (files.map do |file|
660
- menu_with_block_labels(filename: file,
661
- struct: true)
662
- end).flatten(1)
663
- end,
664
- list_default_yaml: -> { fout_list list_default_yaml },
665
- list_docs: -> { fout_list files },
666
- 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 },
667
248
  list_recent_output: lambda {
668
- fout_list list_recent_output(
249
+ @fout.fout_list list_recent_output(
669
250
  @options[:saved_stdout_folder],
670
251
  @options[:saved_stdout_glob], @options[:list_count]
671
252
  )
672
253
  },
673
254
  list_recent_scripts: lambda {
674
- fout_list list_recent_scripts(
255
+ @fout.fout_list list_recent_scripts(
675
256
  options[:saved_script_folder],
676
257
  options[:saved_script_glob], options[:list_count]
677
258
  )
678
259
  },
679
- pwd: -> { fout File.expand_path('..', __dir__) },
260
+ pwd: -> { @fout.fout File.expand_path('..', __dir__) },
680
261
  run_last_script: -> { run_last_script },
681
262
  select_recent_output: -> { select_recent_output },
682
263
  select_recent_script: -> { select_recent_script },
683
- tab_completions: -> { fout tab_completions },
684
- menu_export: -> { fout menu_export }
264
+ tab_completions: -> { @fout.fout tab_completions },
265
+ menu_export: -> { @fout.fout menu_export }
685
266
  }
686
267
 
687
268
  return if execute_simple_commands(simple_commands)
688
269
 
689
- files = prepare_file_list(options)
270
+ files = opts_prepare_file_list(options)
690
271
  execute_block_logic(files)
691
272
  return unless @options[:output_saved_script_filename]
692
273
 
693
- fout "saved_filespec: #{@execute_script_filespec}"
694
- rescue StandardError => err
695
- warn(error = "ERROR ** MarkParse.execute_code_block_based_on_options(); #{err.inspect}")
696
- binding.pry if $tap_enable
697
- 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')
698
278
  end
699
279
 
700
280
  # Executes command based on the provided option keys
@@ -727,176 +307,22 @@ module MarkdownExec
727
307
  #
728
308
  block_name = rest.shift
729
309
  @options[:block_name] = block_name if block_name.present?
730
- end
731
-
732
- # Formats multiline body content as a title string.
733
- # @param body_lines [Array<String>] The lines of body content.
734
- # @return [String] Formatted title.
735
- def format_multiline_body_as_title(body_lines)
736
- body_lines.map.with_index do |line, index|
737
- index.zero? ? line : " #{line}"
738
- end.join("\n") << "\n"
739
- end
740
-
741
- ## summarize blocks
742
- #
743
- def get_block_summary(call_options, fcb)
744
- opts = optsmerge call_options
745
- # return fcb.body unless opts[:struct]
746
- return fcb unless opts[:bash]
747
-
748
- fcb.call = fcb.title.match(Regexp.new(opts[:block_calls_scan]))&.fetch(1, nil)
749
- titlexcall = if fcb.call
750
- fcb.title.sub("%#{fcb.call}", '')
751
- else
752
- fcb.title
753
- end
754
- bm = extract_named_captures_from_option(titlexcall,
755
- opts[:block_name_match])
756
- fcb.stdin = extract_named_captures_from_option(titlexcall,
757
- opts[:block_stdin_scan])
758
- fcb.stdout = extract_named_captures_from_option(titlexcall,
759
- opts[:block_stdout_scan])
760
-
761
- shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
762
- fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall
763
- fcb.dname = if shell_color_option && opts[shell_color_option].present?
764
- fcb.oname.send(opts[shell_color_option].to_sym)
765
- else
766
- fcb.oname
767
- end
768
- fcb
769
- end
770
-
771
- # Handles the link-back operation.
772
- #
773
- # @param opts [Hash] Configuration options hash.
774
- # @return [Array<Symbol, String>] A tuple containing a LoadFile flag and an empty string.
775
- def handle_back_link(opts)
776
- history_state_pop(opts)
777
- [LoadFile::Load, '']
778
- end
779
-
780
- # Handles the execution and display of remainder blocks from a selected menu item.
781
- #
782
- # @param mdoc [Object] Document object containing code blocks.
783
- # @param opts [Hash] Configuration options hash.
784
- # @param selected [Hash] Selected item from the menu.
785
- # @return [Array<Symbol, String>] A tuple containing a LoadFile flag and an empty string.
786
- # @note The function can prompt the user for approval before executing code if opts[:user_must_approve] is true.
787
- def handle_remainder_blocks(mdoc, opts, selected)
788
- required_lines = collect_required_code_lines(mdoc, selected,
789
- opts: opts)
790
- if opts[:output_script] || opts[:user_must_approve]
791
- display_required_code(opts, required_lines)
792
- end
793
- allow = if opts[:user_must_approve]
794
- prompt_for_user_approval(opts,
795
- required_lines)
796
- else
797
- true
798
- end
799
- opts[:s_ir_approve] = allow
800
- if opts[:s_ir_approve]
801
- execute_approved_block(opts,
802
- required_lines)
803
- end
804
-
805
- [LoadFile::Reuse, '']
806
- end
807
-
808
- # Handles the link-shell operation.
809
- #
810
- # @param opts [Hash] Configuration options hash.
811
- # @param body [Array<String>] The body content.
812
- # @param mdoc [Object] Document object containing code blocks.
813
- # @return [Array<Symbol, String>] A tuple containing a LoadFile flag and a block name.
814
- def handle_shell_link(opts, body, mdoc)
815
- data = body.present? ? YAML.load(body.join("\n")) : {}
816
- data_file = data.fetch('file', nil)
817
- return [LoadFile::Reuse, ''] unless data_file
818
-
819
- history_state_push(mdoc, data_file, opts)
820
-
821
- data.fetch('vars', []).each do |var|
822
- ENV[var[0]] = var[1].to_s
823
- end
824
-
825
- [LoadFile::Load, data.fetch('block', '')]
826
- end
827
-
828
- # Handles options for the shell.
829
- #
830
- # @param opts [Hash] Configuration options hash.
831
- # @param selected [Hash] Selected item from the menu.
832
- # @return [Array<Symbol, String>] A tuple containing a LoadFile::Reuse flag and an empty string.
833
- def handle_shell_opts(opts, selected, tgt2 = nil)
834
- data = YAML.load(selected[:body].join("\n"))
835
- data.each_key do |key|
836
- opts[key.to_sym] = value = data[key]
837
- tgt2[key.to_sym] = value if tgt2
838
- next unless opts[:menu_opts_set_format].present?
839
-
840
- print format(
841
- opts[:menu_opts_set_format],
842
- { key: key,
843
- value: value }
844
- ).send(opts[:menu_opts_set_color].to_sym)
845
- end
846
- [LoadFile::Reuse, '']
847
- end
848
-
849
- # Handles reading and processing lines from a given IO stream
850
- #
851
- # @param stream [IO] The IO stream to read from (e.g., stdout, stderr, stdin).
852
- # @param file_type [Symbol] The type of file to which the stream corresponds.
853
- def handle_stream(opts, stream, file_type, swap: false)
854
- Thread.new do
855
- until (line = stream.gets).nil?
856
- @execute_files[file_type] =
857
- @execute_files[file_type] + [line.strip]
858
- print line if opts[:output_stdout]
859
- yield line if block_given?
860
- end
861
- rescue IOError
862
- #d 'stdout IOError, thread killed, do nothing'
863
- end
864
- end
865
-
866
- def history_state_exist?
867
- history = ENV.fetch(MDE_HISTORY_ENV_NAME, '')
868
- history.present? ? history : nil
869
- end
870
-
871
- def history_state_partition(opts)
872
- unit, rest = StringUtil.partition_at_first(
873
- ENV.fetch(MDE_HISTORY_ENV_NAME, ''),
874
- opts[:history_document_separator]
875
- )
876
- { unit: unit, rest: rest }.tap_inspect
877
- end
878
-
879
- def history_state_pop(opts)
880
- state = history_state_partition(opts)
881
- opts[:filename] = state[:unit]
882
- ENV[MDE_HISTORY_ENV_NAME] = state[:rest]
883
- delete_required_temp_file
884
- end
885
-
886
- def history_state_push(mdoc, data_file, opts)
887
- [data_file, opts[:block_name]].tap_inspect 'filename, blockname'
888
- new_history = opts[:filename] +
889
- opts[:history_document_separator] +
890
- ENV.fetch(MDE_HISTORY_ENV_NAME, '')
891
- opts[:filename] = data_file
892
- write_required_blocks_to_temp_file(mdoc, opts[:block_name], opts)
893
- ENV[MDE_HISTORY_ENV_NAME] = new_history
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')
894
318
  end
895
319
 
896
320
  ## Sets up the options and returns the parsed arguments
897
321
  #
898
322
  def initialize_and_parse_cli_options
899
- @options = base_options
323
+ # @options = base_options
324
+ @options = HashDelegator.new(base_options)
325
+
900
326
  read_configuration_file!(@options,
901
327
  ".#{MarkdownExec::APP_NAME.downcase}.yml")
902
328
 
@@ -909,7 +335,7 @@ module MarkdownExec
909
335
  ].join("\n")
910
336
 
911
337
  menu_iter do |item|
912
- menu_option_append opts, @options, item
338
+ opts_menu_option_append opts, @options, item
913
339
  end
914
340
  end
915
341
  @option_parser.load
@@ -917,55 +343,11 @@ module MarkdownExec
917
343
 
918
344
  rest = @option_parser.parse!(arguments_for_mde)
919
345
  @options[:s_pass_args] = ARGV[rest.count + 1..]
346
+ @options.merge(@options.run_state.to_h)
920
347
 
921
348
  rest
922
349
  end
923
350
 
924
- def initialize_and_save_execution_output
925
- return unless @options[:save_execution_output]
926
-
927
- @options[:logged_stdout_filename] =
928
- SavedAsset.stdout_name(blockname: @options[:block_name],
929
- filename: File.basename(@options[:filename],
930
- '.*'),
931
- prefix: @options[:logged_stdout_filename_prefix],
932
- time: Time.now.utc)
933
-
934
- @logged_stdout_filespec =
935
- @options[:logged_stdout_filespec] =
936
- File.join @options[:saved_stdout_folder],
937
- @options[:logged_stdout_filename]
938
- @logged_stdout_filespec = @options[:logged_stdout_filespec]
939
- write_execution_output_to_file
940
- end
941
-
942
- # Initializes variables for regex and other states
943
- def initialize_state(opts)
944
- {
945
- fenced_start_and_end_regex: Regexp.new(opts[:fenced_start_and_end_regex]),
946
- fenced_start_extended_regex: Regexp.new(opts[:fenced_start_extended_regex]),
947
- fcb: FCB.new,
948
- in_fenced_block: false,
949
- headings: []
950
- }
951
- end
952
-
953
- # Main function to iterate through blocks in file
954
- def iter_blocks_in_file(opts = {}, &block)
955
- return unless check_file_existence(opts[:filename])
956
-
957
- state = initialize_state(opts)
958
-
959
- selected_messages = yield :filter
960
-
961
- cfile.readlines(opts[:filename]).each do |line|
962
- next unless line
963
-
964
- update_line_and_block_state(line, state, opts, selected_messages,
965
- &block)
966
- end
967
- end
968
-
969
351
  ##
970
352
  # Returns a lambda expression based on the given procname.
971
353
  # @param procname [String] The name of the process to generate a lambda for.
@@ -981,7 +363,7 @@ module MarkdownExec
981
363
  ->(_) { exit }
982
364
  when 'help'
983
365
  lambda { |_|
984
- fout menu_help
366
+ @fout.fout menu_help
985
367
  exit
986
368
  }
987
369
  when 'path'
@@ -989,7 +371,7 @@ module MarkdownExec
989
371
  when 'show_config'
990
372
  lambda { |_|
991
373
  finalize_cli_argument_processing(options)
992
- fout options.sort_by_key.to_yaml
374
+ @fout.fout options.sort_by_key.to_yaml
993
375
  }
994
376
  when 'val_as_bool'
995
377
  lambda { |value|
@@ -1001,7 +383,7 @@ module MarkdownExec
1001
383
  ->(value) { value.to_s }
1002
384
  when 'version'
1003
385
  lambda { |_|
1004
- fout MarkdownExec::VERSION
386
+ @fout.fout MarkdownExec::VERSION
1005
387
  exit
1006
388
  }
1007
389
  else
@@ -1031,16 +413,7 @@ module MarkdownExec
1031
413
  end.compact.sort
1032
414
  end
1033
415
 
1034
- def list_files_per_options(options)
1035
- list_files_specified(
1036
- determine_filename(
1037
- specified_filename: options[:filename]&.present? ? options[:filename] : nil,
1038
- specified_folder: options[:path],
1039
- default_filename: 'README.md',
1040
- default_folder: '.'
1041
- )
1042
- )
1043
- end
416
+ public
1044
417
 
1045
418
  ## Searches for files based on the specified or default filenames and folders
1046
419
  #
@@ -1057,105 +430,7 @@ module MarkdownExec
1057
430
  @options[:md_filename_glob]))
1058
431
  end
1059
432
 
1060
- ## output type (body string or full object) per option struct and bash
1061
- #
1062
- def list_named_blocks_in_file(call_options = {}, &options_block)
1063
- opts = optsmerge call_options, options_block
1064
- blocks_per_opts(
1065
- menu_from_file(opts.merge(struct: true)).select do |fcb|
1066
- Filter.fcb_select?(opts.merge(no_chrome: true), fcb)
1067
- end, opts
1068
- )
1069
- end
1070
-
1071
- # return true if values were modified
1072
- # execute block once per filename
1073
- #
1074
- def load_auto_blocks(opts, blocks_in_file)
1075
- return unless opts[:document_load_opts_block_name].present?
1076
- return if opts[:s_most_recent_filename] == opts[:filename]
1077
-
1078
- block = block_find(blocks_in_file, :oname,
1079
- opts[:document_load_opts_block_name])
1080
- return unless block
1081
-
1082
- handle_shell_opts(opts, block, @options)
1083
- opts[:s_most_recent_filename] = opts[:filename]
1084
- true
1085
- end
1086
-
1087
- def mdoc_and_menu_from_file(opts)
1088
- menu_blocks = menu_from_file(opts.merge(struct: true))
1089
- mdoc = MDoc.new(menu_blocks) do |nopts|
1090
- opts.merge!(nopts)
1091
- end
1092
- [menu_blocks, mdoc]
1093
- end
1094
-
1095
- ## Handles the file loading and returns the blocks in the file and MDoc instance
1096
- #
1097
- def mdoc_menu_and_selected_from_file(opts)
1098
- blocks_in_file, mdoc = mdoc_and_menu_from_file(opts)
1099
- if load_auto_blocks(opts, blocks_in_file)
1100
- # recreate menu with new options
1101
- #
1102
- blocks_in_file, mdoc = mdoc_and_menu_from_file(opts)
1103
- end
1104
-
1105
- blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
1106
- add_menu_chrome_blocks!(blocks_menu)
1107
- [blocks_in_file, blocks_menu, mdoc]
1108
- end
1109
-
1110
- def menu_chrome_colored_option(opts,
1111
- option_symbol = :menu_option_back_name)
1112
- if opts[:menu_chrome_color]
1113
- menu_chrome_formatted_option(opts,
1114
- option_symbol).send(opts[:menu_chrome_color].to_sym)
1115
- else
1116
- menu_chrome_formatted_option(opts, option_symbol)
1117
- end
1118
- end
1119
-
1120
- def menu_chrome_formatted_option(opts,
1121
- option_symbol = :menu_option_back_name)
1122
- val1 = safeval(opts.fetch(option_symbol, ''))
1123
- val1 unless opts[:menu_chrome_format]
1124
-
1125
- format(opts[:menu_chrome_format], val1)
1126
- end
1127
-
1128
- def menu_export(data = menu_for_optparse)
1129
- data.map do |item|
1130
- item.delete(:procname)
1131
- item
1132
- end.to_yaml
1133
- end
1134
-
1135
- def menu_for_blocks(menu_options)
1136
- options = calculated_options.merge menu_options
1137
- menu = []
1138
- iter_blocks_in_file(options) do |btype, fcb|
1139
- case btype
1140
- when :filter
1141
- %i[blocks line]
1142
- when :line
1143
- if options[:menu_divider_match] &&
1144
- (mbody = fcb.body[0].match(options[:menu_divider_match]))
1145
- menu.push FCB.new({ dname: mbody[:name], oname: mbody[:name],
1146
- disabled: '' })
1147
- end
1148
- if options[:menu_note_match] &&
1149
- (mbody = fcb.body[0].match(options[:menu_note_match]))
1150
- menu.push FCB.new({ dname: mbody[:name], oname: mbody[:name],
1151
- disabled: '' })
1152
- end
1153
- when :blocks
1154
- menu += [fcb.oname]
1155
- end
1156
- end
1157
- menu
1158
- end
433
+ private
1159
434
 
1160
435
  ##
1161
436
  # Generates a menu suitable for OptionParser from the menu items defined in YAML format.
@@ -1169,37 +444,6 @@ module MarkdownExec
1169
444
  end
1170
445
  end
1171
446
 
1172
- ##
1173
- # Returns a list of blocks in a given file, including dividers, tasks, and other types of blocks.
1174
- # The list can be customized via call_options and options_block.
1175
- #
1176
- # @param call_options [Hash] Options passed as an argument.
1177
- # @param options_block [Proc] Block for dynamic option manipulation.
1178
- # @return [Array<FCB>] An array of FCB objects representing the blocks.
1179
- #
1180
- def menu_from_file(call_options = {},
1181
- &options_block)
1182
- opts = optsmerge(call_options, options_block)
1183
- use_chrome = !opts[:no_chrome]
1184
-
1185
- blocks = []
1186
- iter_blocks_in_file(opts) do |btype, fcb|
1187
- case btype
1188
- when :blocks
1189
- append_block_summary(blocks, fcb, opts)
1190
- when :filter # what btypes are responded to?
1191
- %i[blocks line]
1192
- when :line
1193
- create_and_add_chrome_blocks(blocks, fcb, opts, use_chrome)
1194
- end
1195
- end
1196
- blocks
1197
- rescue StandardError => err
1198
- warn(error = "ERROR ** MarkParse.menu_from_file(); #{err.inspect}")
1199
- warn(caller[0..4])
1200
- raise StandardError, error
1201
- end
1202
-
1203
447
  def menu_help
1204
448
  @option_parser.help
1205
449
  end
@@ -1208,10 +452,26 @@ module MarkdownExec
1208
452
  data.map(&block)
1209
453
  end
1210
454
 
1211
- def menu_option_append(opts, options, item)
1212
- unless item[:long_name].present? || item[:short_name].present?
1213
- return
1214
- 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?
1215
475
 
1216
476
  opts.on(*[
1217
477
  # - long name
@@ -1239,188 +499,9 @@ module MarkdownExec
1239
499
  ].compact)
1240
500
  end
1241
501
 
1242
- def menu_with_block_labels(call_options = {})
1243
- opts = options.merge(call_options)
1244
- menu_from_file(opts).map do |fcb|
1245
- BlockLabel.make(
1246
- filename: opts[:filename],
1247
- headings: fcb.fetch(:headings, []),
1248
- menu_blocks_with_docname: opts[:menu_blocks_with_docname],
1249
- menu_blocks_with_headings: opts[:menu_blocks_with_headings],
1250
- title: fcb[:title],
1251
- text: fcb[:text],
1252
- body: fcb[:body]
1253
- )
1254
- end.compact
1255
- end
1256
-
1257
- def next_block_name_from_command_line_arguments(opts)
1258
- if opts[:s_cli_rest].present?
1259
- opts[:block_name] = opts[:s_cli_rest].pop
1260
- false # repeat_menu
1261
- else
1262
- true # repeat_menu
1263
- end
1264
- end
1265
-
1266
- # :reek:ControlParameter
1267
- def optsmerge(call_options = {}, options_block = nil)
1268
- class_call_options = @options.merge(call_options || {})
1269
- if options_block
1270
- options_block.call class_call_options
1271
- else
1272
- class_call_options
1273
- end
1274
- end
1275
-
1276
- def output_execution_result
1277
- oq = [['Block', @options[:block_name], DISPLAY_LEVEL_ADMIN],
1278
- ['Command',
1279
- [MarkdownExec::BIN_NAME,
1280
- @options[:filename],
1281
- @options[:block_name]].join(' '),
1282
- DISPLAY_LEVEL_ADMIN]]
1283
-
1284
- [['Script', :saved_filespec],
1285
- ['StdOut', :logged_stdout_filespec]].each do |label, name|
1286
- if @options[name]
1287
- oq << [label, @options[name],
1288
- DISPLAY_LEVEL_ADMIN]
1289
- end
1290
- end
1291
-
1292
- oq.map do |label, value, level|
1293
- lout ["#{label}:".yellow, value.to_s].join(' '), level: level
1294
- end
1295
- end
1296
-
1297
- def output_execution_summary
1298
- return unless @options[:output_execution_summary]
1299
-
1300
- fout_section 'summary', {
1301
- execute_aborted_at: @execute_aborted_at,
1302
- execute_completed_at: @execute_completed_at,
1303
- execute_error: @execute_error,
1304
- execute_error_message: @execute_error_message,
1305
- execute_files: @execute_files,
1306
- execute_options: @execute_options,
1307
- execute_started_at: @execute_started_at,
1308
- execute_script_filespec: @execute_script_filespec
1309
- }
1310
- end
1311
-
1312
- # Prepare the blocks menu by adding labels and other necessary details.
1313
- #
1314
- # @param blocks_in_file [Array<Hash>] The list of blocks from the file.
1315
- # @param opts [Hash] The options hash.
1316
- # @return [Array<Hash>] The updated blocks menu.
1317
- def prepare_blocks_menu(blocks_in_file, opts)
1318
- # next if fcb.fetch(:disabled, false)
1319
- # next unless fcb.fetch(:name, '').present?
1320
- replace_consecutive_blanks(blocks_in_file).map do |fcb|
1321
- next if Filter.prepared_not_in_menu?(opts, fcb)
1322
-
1323
- fcb.merge!(
1324
- name: fcb.dname,
1325
- label: BlockLabel.make(
1326
- body: fcb[:body],
1327
- filename: opts[:filename],
1328
- headings: fcb.fetch(:headings, []),
1329
- menu_blocks_with_docname: opts[:menu_blocks_with_docname],
1330
- menu_blocks_with_headings: opts[:menu_blocks_with_headings],
1331
- text: fcb[:text],
1332
- title: fcb[:title]
1333
- )
1334
- )
1335
- fcb.to_h
1336
- end.compact
1337
- end
1338
-
1339
502
  # Prepares and fetches file listings
1340
- def prepare_file_list(options)
1341
- list_files_per_options(options)
1342
- end
1343
-
1344
- def process_fenced_block(fcb, opts, selected_messages, &block)
1345
- fcb.oname = fcb.dname = fcb.title || ''
1346
- return unless fcb.body
1347
-
1348
- update_title_from_body(fcb)
1349
-
1350
- if block &&
1351
- selected_messages.include?(:blocks) &&
1352
- Filter.fcb_select?(opts, fcb)
1353
- block.call :blocks, fcb
1354
- end
1355
- end
1356
-
1357
- def process_line(line, _opts, selected_messages, &block)
1358
- return unless block && selected_messages.include?(:line)
1359
-
1360
- # dp 'text outside of fcb'
1361
- fcb = FCB.new
1362
- fcb.body = [line]
1363
- block.call(:line, fcb)
1364
- end
1365
-
1366
- class MenuOptions
1367
- YES = 1
1368
- NO = 2
1369
- SCRIPT_TO_CLIPBOARD = 3
1370
- SAVE_SCRIPT = 4
1371
- end
1372
-
1373
- ##
1374
- # Presents a menu to the user for approving an action and performs additional tasks based on the selection.
1375
- # The function provides options for approval, rejection, copying data to clipboard, or saving data to a file.
1376
- #
1377
- # @param opts [Hash] A hash containing various options for the menu.
1378
- # @param required_lines [Array<String>] Lines of text or code that are subject to user approval.
1379
- #
1380
- # @option opts [String] :prompt_approve_block Prompt text for the approval menu.
1381
- # @option opts [String] :prompt_yes Text for the 'Yes' choice in the menu.
1382
- # @option opts [String] :prompt_no Text for the 'No' choice in the menu.
1383
- # @option opts [String] :prompt_script_to_clipboard Text for the 'Copy to Clipboard' choice in the menu.
1384
- # @option opts [String] :prompt_save_script Text for the 'Save to File' choice in the menu.
1385
- #
1386
- # @return [Boolean] Returns true if the user approves (selects 'Yes'), false otherwise.
1387
- ##
1388
- def prompt_for_user_approval(opts, required_lines)
1389
- # Present a selection menu for user approval.
1390
-
1391
- sel = @prompt.select(opts[:prompt_approve_block],
1392
- filter: true) do |menu|
1393
- menu.default MenuOptions::YES
1394
- menu.choice opts[:prompt_yes], MenuOptions::YES
1395
- menu.choice opts[:prompt_no], MenuOptions::NO
1396
- menu.choice opts[:prompt_script_to_clipboard],
1397
- MenuOptions::SCRIPT_TO_CLIPBOARD
1398
- menu.choice opts[:prompt_save_script], MenuOptions::SAVE_SCRIPT
1399
- end
1400
-
1401
- if sel == MenuOptions::SCRIPT_TO_CLIPBOARD
1402
- copy_to_clipboard(required_lines)
1403
- elsif sel == MenuOptions::SAVE_SCRIPT
1404
- save_to_file(opts, required_lines)
1405
- end
1406
-
1407
- sel == MenuOptions::YES
1408
- rescue TTY::Reader::InputInterrupt
1409
- exit 1
1410
- end
1411
-
1412
- def prompt_select_continue(opts)
1413
- sel = @prompt.select(
1414
- opts[:prompt_after_bash_exec],
1415
- filter: true,
1416
- quiet: true
1417
- ) do |menu|
1418
- menu.choice opts[:prompt_yes]
1419
- menu.choice opts[:prompt_exit]
1420
- end
1421
- sel == opts[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
1422
- rescue TTY::Reader::InputInterrupt
1423
- exit 1
503
+ def opts_prepare_file_list(options)
504
+ opts_list_files(options)
1424
505
  end
1425
506
 
1426
507
  # :reek:UtilityFunction ### temp
@@ -1431,53 +512,18 @@ module MarkdownExec
1431
512
  .transform_keys(&:to_sym))
1432
513
  end
1433
514
 
1434
- # Reads required code blocks from a temporary file specified by an environment variable.
1435
- #
1436
- # @return [Array<String>] An array containing the lines read from the temporary file.
1437
- # @note Relies on the 'MDE_LINK_REQUIRED_FILE' environment variable to locate the file.
1438
- def read_required_blocks_from_temp_file
1439
- temp_blocks = []
1440
-
1441
- temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
1442
- if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
1443
- return temp_blocks
1444
- end
1445
-
1446
- if File.exist?(temp_blocks_file_path)
1447
- temp_blocks = File.readlines(temp_blocks_file_path, chomp: true)
1448
- end
1449
-
1450
- temp_blocks
1451
- end
1452
-
1453
- # Replace duplicate blanks (where :oname is not present) with a single blank line.
1454
- #
1455
- # @param [Array<Hash>] lines Array of hashes to process.
1456
- # @return [Array<Hash>] Cleaned array with consecutive blanks collapsed into one.
1457
- def replace_consecutive_blanks(lines)
1458
- lines.chunk_while do |i, j|
1459
- i[:oname].to_s.empty? && j[:oname].to_s.empty?
1460
- end.map do |chunk|
1461
- if chunk.any? do |line|
1462
- line[:oname].to_s.strip.empty?
1463
- end
1464
- chunk.first
1465
- else
1466
- chunk
1467
- end
1468
- end.flatten
1469
- end
515
+ public
1470
516
 
1471
517
  def run
1472
518
  clear_required_file
1473
519
  execute_block_with_error_handling(initialize_and_parse_cli_options)
1474
- delete_required_temp_file
1475
- rescue StandardError => err
1476
- warn(error = "ERROR ** MarkParse.run(); #{err.inspect}")
1477
- binding.pry if $tap_enable
1478
- raise ArgumentError, error
520
+ @options.delete_required_temp_file
521
+ rescue StandardError
522
+ error_handler('run')
1479
523
  end
1480
524
 
525
+ private
526
+
1481
527
  def run_last_script
1482
528
  filename = SavedFilesMatcher.most_recent(@options[:saved_script_folder],
1483
529
  @options[:saved_script_glob])
@@ -1485,89 +531,20 @@ module MarkdownExec
1485
531
 
1486
532
  saved_name_split filename
1487
533
  @options[:save_executed_script] = false
1488
- select_approve_and_execute_block
1489
- end
1490
-
1491
- def safeval(str)
1492
- eval(str)
534
+ @options.select_approve_and_execute_block
1493
535
  rescue StandardError
1494
- warn $!
1495
- binding.pry if $tap_enable
1496
- raise StandardError, $!
1497
- end
1498
-
1499
- def save_to_file(opts, required_lines)
1500
- write_command_file(opts.merge(save_executed_script: true),
1501
- required_lines)
1502
- fout "File saved: #{@options[:saved_filespec]}"
536
+ error_handler('run_last_script')
1503
537
  end
1504
538
 
1505
539
  def saved_name_split(name)
1506
540
  # rubocop:disable Layout/LineLength
1507
- 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)
1508
542
  # rubocop:enable Layout/LineLength
1509
543
  return unless mf
1510
544
 
1511
545
  @options[:block_name] = mf[:block]
1512
- @options[:filename] = mf[:file].gsub(FNR12, FNR11)
1513
- end
1514
-
1515
- # Select and execute a code block from a Markdown document.
1516
- #
1517
- # This method allows the user to interactively select a code block from a
1518
- # Markdown document, obtain approval, and execute the chosen block of code.
1519
- #
1520
- # @param call_options [Hash] Initial options for the method.
1521
- # @param options_block [Block] Block of options to be merged with call_options.
1522
- # @return [Nil] Returns nil if no code block is selected or an error occurs.
1523
- def select_approve_and_execute_block(call_options = {},
1524
- &options_block)
1525
- base_opts = optsmerge(call_options, options_block)
1526
- repeat_menu = true && !base_opts[:block_name].present?
1527
- load_file = LoadFile::Reuse
1528
- default = nil
1529
- block = nil
1530
-
1531
- loop do
1532
- loop do
1533
- opts = base_opts.dup
1534
- opts[:s_back] = false
1535
- blocks_in_file, blocks_menu, mdoc = mdoc_menu_and_selected_from_file(opts)
1536
- block, state = command_or_user_selected_block(blocks_in_file, blocks_menu,
1537
- default, opts)
1538
- return if state == MenuState::EXIT
1539
-
1540
- load_file, next_block_name = approve_and_execute_block(block, opts,
1541
- mdoc)
1542
- default = load_file == LoadFile::Load ? nil : opts[:block_name]
1543
- base_opts[:block_name] = opts[:block_name] = next_block_name
1544
- base_opts[:filename] = opts[:filename]
1545
-
1546
- # user prompt to exit if the menu will be displayed again
1547
- #
1548
- if repeat_menu &&
1549
- block[:shell] == BlockType::BASH &&
1550
- opts[:pause_after_bash_exec] &&
1551
- prompt_select_continue(opts) == MenuState::EXIT
1552
- return
1553
- end
1554
-
1555
- # exit current document/menu if loading next document or single block_name was specified
1556
- #
1557
- if state == MenuState::CONTINUE && load_file == LoadFile::Load
1558
- break
1559
- end
1560
- break unless repeat_menu
1561
- end
1562
- break if load_file == LoadFile::Reuse
1563
-
1564
- repeat_menu = next_block_name_from_command_line_arguments(base_opts)
1565
- end
1566
- rescue StandardError => err
1567
- warn(error = "ERROR ** MarkParse.select_approve_and_execute_block(); #{err.inspect}")
1568
- warn err.backtrace
1569
- binding.pry if $tap_enable
1570
- raise ArgumentError, error
546
+ @options[:filename] = mf[:file].gsub(@options[:saved_filename_pattern],
547
+ @options[:saved_filename_replacement])
1571
548
  end
1572
549
 
1573
550
  def select_document_if_multiple(files = list_markdown_files_in_path)
@@ -1576,44 +553,24 @@ module MarkdownExec
1576
553
  return unless count >= 2
1577
554
 
1578
555
  opts = options.dup
1579
- select_option_or_exit opts[:prompt_select_md].to_s, files,
1580
- 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]))
1581
559
  end
1582
560
 
1583
561
  # Presents a TTY prompt to select an option or exit, returns selected option or nil
1584
- def select_option_or_exit(prompt_text, items, opts = {})
1585
- 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)
1586
565
  return unless result.fetch(:option, nil)
1587
566
 
1588
567
  result[:selected]
1589
568
  end
1590
569
 
1591
- # Presents a TTY prompt to select an option or exit, returns metadata including option and selected
1592
- def select_option_with_metadata(prompt_text, items, opts = {})
1593
- selection = @prompt.select(prompt_text,
1594
- items,
1595
- opts.merge(filter: true))
1596
-
1597
- items.find { |item| item[:dname] == selection }
1598
- .merge(
1599
- if selection == menu_chrome_colored_option(opts,
1600
- :menu_option_back_name)
1601
- { option: selection, curr: @hs_curr, rest: @hs_rest,
1602
- shell: BlockType::LINK }
1603
- elsif selection == menu_chrome_colored_option(opts,
1604
- :menu_option_exit_name)
1605
- { option: selection }
1606
- else
1607
- { selected: selection }
1608
- end
1609
- )
1610
- rescue TTY::Reader::InputInterrupt
1611
- exit 1
1612
- end
1613
-
1614
570
  def select_recent_output
1615
571
  filename = select_option_or_exit(
1616
- @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),
1617
574
  list_recent_output(
1618
575
  @options[:saved_stdout_folder],
1619
576
  @options[:saved_stdout_glob],
@@ -1628,7 +585,8 @@ module MarkdownExec
1628
585
 
1629
586
  def select_recent_script
1630
587
  filename = select_option_or_exit(
1631
- @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),
1632
590
  list_recent_scripts(
1633
591
  @options[:saved_script_folder],
1634
592
  @options[:saved_script_glob],
@@ -1640,37 +598,10 @@ module MarkdownExec
1640
598
 
1641
599
  saved_name_split(filename)
1642
600
 
1643
- select_approve_and_execute_block({ bash: true,
1644
- save_executed_script: false,
1645
- struct: true })
601
+ @options.select_approve_and_execute_block ### ({ save_executed_script: false })
1646
602
  end
1647
603
 
1648
- def start_fenced_block(opts, line, headings,
1649
- fenced_start_extended_regex)
1650
- fcb_title_groups = line.match(fenced_start_extended_regex).named_captures.sym_keys
1651
- rest = fcb_title_groups.fetch(:rest, '')
1652
-
1653
- fcb = FCB.new
1654
- fcb.headings = headings
1655
- fcb.oname = fcb.dname = fcb_title_groups.fetch(:name, '')
1656
- fcb.shell = fcb_title_groups.fetch(:shell, '')
1657
- fcb.title = fcb_title_groups.fetch(:name, '')
1658
- fcb.body = []
1659
- fcb.reqs, fcb.wraps =
1660
- ArrayUtil.partition_by_predicate(rest.scan(/\+[^\s]+/).map do |req|
1661
- req[1..-1]
1662
- end) do |name|
1663
- !name.match(Regexp.new(opts[:block_name_wrapper_match]))
1664
- end
1665
- fcb.call = rest.match(Regexp.new(opts[:block_calls_scan]))&.to_a&.first
1666
- fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
1667
- tn.named_captures.sym_keys
1668
- end
1669
- fcb.stdout = if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
1670
- tn.named_captures.sym_keys
1671
- end
1672
- fcb
1673
- end
604
+ public
1674
605
 
1675
606
  def tab_completions(data = menu_for_optparse)
1676
607
  data.map do |item|
@@ -1678,83 +609,6 @@ module MarkdownExec
1678
609
  end.compact
1679
610
  end
1680
611
 
1681
- def tty_prompt_without_disabled_symbol
1682
- TTY::Prompt.new(interrupt: lambda {
1683
- puts;
1684
- raise TTY::Reader::InputInterrupt
1685
- },
1686
- symbols: { cross: ' ' })
1687
- end
1688
-
1689
- ##
1690
- # Updates the hierarchy of document headings based on the given line and existing headings.
1691
- # The function uses regular expressions specified in the `opts` to identify different levels of headings.
1692
- #
1693
- # @param line [String] The line of text to examine for heading content.
1694
- # @param headings [Array<String>] The existing list of document headings.
1695
- # @param opts [Hash] A hash containing options for regular expression matches for different heading levels.
1696
- #
1697
- # @option opts [String] :heading1_match Regular expression for matching first-level headings.
1698
- # @option opts [String] :heading2_match Regular expression for matching second-level headings.
1699
- # @option opts [String] :heading3_match Regular expression for matching third-level headings.
1700
- #
1701
- # @return [Array<String>] Updated list of headings.
1702
- def update_document_headings(line, headings, opts)
1703
- if (lm = line.match(Regexp.new(opts[:heading3_match])))
1704
- [headings[0], headings[1], lm[:name]]
1705
- elsif (lm = line.match(Regexp.new(opts[:heading2_match])))
1706
- [headings[0], lm[:name]]
1707
- elsif (lm = line.match(Regexp.new(opts[:heading1_match])))
1708
- [lm[:name]]
1709
- else
1710
- headings
1711
- end
1712
- end
1713
-
1714
- ##
1715
- # Processes an individual line within a loop, updating headings and handling fenced code blocks.
1716
- # This function is designed to be called within a loop that iterates through each line of a document.
1717
- #
1718
- # @param line [String] The current line being processed.
1719
- # @param state [Hash] The current state of the parser, including flags and data related to the processing.
1720
- # @param opts [Hash] A hash containing various options for line and block processing.
1721
- # @param selected_messages [Array<String>] Accumulator for lines or messages that are subject to further processing.
1722
- # @param block [Proc] An optional block for further processing or transformation of lines.
1723
- #
1724
- # @option state [Array<String>] :headings Current headings to be updated based on the line.
1725
- # @option state [Regexp] :fenced_start_and_end_regex Regular expression to match the start and end of a fenced block.
1726
- # @option state [Boolean] :in_fenced_block Flag indicating whether the current line is inside a fenced block.
1727
- # @option state [Object] :fcb An object representing the current fenced code block being processed.
1728
- #
1729
- # @option opts [Boolean] :menu_blocks_with_headings Flag indicating whether to update headings while processing.
1730
- #
1731
- # @return [Void] The function modifies the `state` and `selected_messages` arguments in place.
1732
- ##
1733
- def update_line_and_block_state(line, state, opts, selected_messages,
1734
- &block)
1735
- if opts[:menu_blocks_with_headings]
1736
- state[:headings] =
1737
- update_document_headings(line, state[:headings], opts)
1738
- end
1739
-
1740
- if line.match(state[:fenced_start_and_end_regex])
1741
- if state[:in_fenced_block]
1742
- process_fenced_block(state[:fcb], opts, selected_messages,
1743
- &block)
1744
- state[:in_fenced_block] = false
1745
- else
1746
- state[:fcb] =
1747
- start_fenced_block(opts, line, state[:headings],
1748
- state[:fenced_start_extended_regex])
1749
- state[:in_fenced_block] = true
1750
- end
1751
- elsif state[:in_fenced_block] && state[:fcb].body
1752
- state[:fcb].body += [line.chomp]
1753
- else
1754
- process_line(line, opts, selected_messages, &block)
1755
- end
1756
- end
1757
-
1758
612
  # :reek:BooleanParameter
1759
613
  # :reek:ControlParameter
1760
614
  def update_options(opts = {}, over: true)
@@ -1765,125 +619,6 @@ module MarkdownExec
1765
619
  end
1766
620
  @options
1767
621
  end
1768
-
1769
- # Updates the title of an FCB object from its body content if the title is nil or empty.
1770
- def update_title_from_body(fcb)
1771
- return unless fcb.title.nil? || fcb.title.empty?
1772
-
1773
- fcb.title = derive_title_from_body(fcb)
1774
- end
1775
-
1776
- def wait_for_user_selected_block(blocks_in_file, blocks_menu,
1777
- default, opts)
1778
- block, state = wait_for_user_selection(blocks_in_file, blocks_menu,
1779
- default, opts)
1780
- case state
1781
- when MenuState::BACK
1782
- opts[:block_name] = block[:dname]
1783
- opts[:s_back] = true
1784
- when MenuState::CONTINUE
1785
- opts[:block_name] = block[:dname]
1786
- end
1787
-
1788
- [block, state]
1789
- end
1790
-
1791
- ## Handles the menu interaction and returns selected block and option state
1792
- #
1793
- def wait_for_user_selection(blocks_in_file, blocks_menu, default,
1794
- opts)
1795
- pt = opts[:prompt_select_block].to_s
1796
- bm = prepare_blocks_menu(blocks_menu, opts)
1797
- return [nil, MenuState::EXIT] if bm.count.zero?
1798
-
1799
- o2 = if default
1800
- opts.merge(default: default)
1801
- else
1802
- opts
1803
- end
1804
-
1805
- obj = select_option_with_metadata(pt, bm, o2.merge(
1806
- per_page: opts[:select_page_height]
1807
- ))
1808
-
1809
- case obj.fetch(:oname, nil)
1810
- when menu_chrome_formatted_option(opts, :menu_option_exit_name)
1811
- [nil, MenuState::EXIT]
1812
- when menu_chrome_formatted_option(opts, :menu_option_back_name)
1813
- [obj, MenuState::BACK]
1814
- else
1815
- [obj, MenuState::CONTINUE]
1816
- end
1817
- rescue StandardError => err
1818
- warn(error = "ERROR ** MarkParse.wait_for_user_selection(); #{err.inspect}")
1819
- warn caller.take(3)
1820
- binding.pry if $tap_enable
1821
- raise ArgumentError, error
1822
- end
1823
-
1824
- # Handles the core logic for generating the command file's metadata and content.
1825
- def write_command_file(call_options, required_lines)
1826
- return unless call_options[:save_executed_script]
1827
-
1828
- time_now = Time.now.utc
1829
- opts = optsmerge call_options
1830
- opts[:saved_script_filename] =
1831
- SavedAsset.script_name(blockname: opts[:block_name],
1832
- filename: opts[:filename],
1833
- prefix: opts[:saved_script_filename_prefix],
1834
- time: time_now)
1835
-
1836
- @execute_script_filespec =
1837
- @options[:saved_filespec] =
1838
- File.join opts[:saved_script_folder],
1839
- opts[:saved_script_filename]
1840
-
1841
- shebang = if @options[:shebang]&.present?
1842
- "#{@options[:shebang]} #{@options[:shell]}\n"
1843
- else
1844
- ''
1845
- end
1846
-
1847
- content = shebang +
1848
- "# file_name: #{opts[:filename]}\n" \
1849
- "# block_name: #{opts[:block_name]}\n" \
1850
- "# time: #{time_now}\n" \
1851
- "#{required_lines.flatten.join("\n")}\n"
1852
-
1853
- create_and_write_file_with_permissions(@options[:saved_filespec], content,
1854
- @options[:saved_script_chmod])
1855
- end
1856
-
1857
- def write_execution_output_to_file
1858
- FileUtils.mkdir_p File.dirname(@options[:logged_stdout_filespec])
1859
-
1860
- ol = ["-STDOUT-\n"]
1861
- ol += @execute_files&.fetch(EF_STDOUT, [])
1862
- ol += ["\n-STDERR-\n"]
1863
- ol += @execute_files&.fetch(EF_STDERR, [])
1864
- ol += ["\n-STDIN-\n"]
1865
- ol += @execute_files&.fetch(EF_STDIN, [])
1866
- ol += ["\n"]
1867
- File.write(@options[:logged_stdout_filespec], ol.join)
1868
- end
1869
-
1870
- # Writes required code blocks to a temporary file and sets an environment variable with its path.
1871
- #
1872
- # @param block_name [String] The name of the block to collect code for.
1873
- # @param opts [Hash] Additional options for collecting code.
1874
- # @note Sets the 'MDE_LINK_REQUIRED_FILE' environment variable to the temporary file path.
1875
- def write_required_blocks_to_temp_file(mdoc, block_name, opts = {})
1876
- code_blocks = (read_required_blocks_from_temp_file +
1877
- mdoc.collect_recursively_required_code(
1878
- block_name,
1879
- opts: opts
1880
- )[:code]).join("\n")
1881
-
1882
- Dir::Tmpname.create(self.class.to_s) do |path|
1883
- File.write(path, code_blocks)
1884
- ENV['MDE_LINK_REQUIRED_FILE'] = path
1885
- end
1886
- end
1887
622
  end # class MarkParse
1888
623
  end # module MarkdownExec
1889
624
 
@@ -1894,60 +629,6 @@ if $PROGRAM_NAME == __FILE__
1894
629
  require 'minitest/autorun'
1895
630
 
1896
631
  module MarkdownExec
1897
- class TestMarkParse < Minitest::Test
1898
- require 'mocha/minitest'
1899
-
1900
- def test_calling_execute_approved_block_calls_command_execute_with_argument_args_value
1901
- pigeon = 'E'
1902
- obj = { s_pass_args: pigeon }
1903
-
1904
- c = MarkdownExec::MarkParse.new
1905
-
1906
- # Expect that method command_execute is called with argument args having value pigeon
1907
- c.expects(:command_execute).with(
1908
- obj,
1909
- '',
1910
- args: pigeon
1911
- )
1912
-
1913
- # Call method execute_approved_block
1914
- c.execute_approved_block(obj, [])
1915
- end
1916
-
1917
- def setup
1918
- @mark_parse = MarkdownExec::MarkParse.new
1919
- end
1920
-
1921
- def test_set_fcb_title
1922
- # sample input and output data for testing update_title_from_body method
1923
- input_output_data = [
1924
- {
1925
- input: FCB.new(title: nil, body: ["puts 'Hello, world!'"]),
1926
- output: "puts 'Hello, world!'"
1927
- },
1928
- {
1929
- input: FCB.new(title: '',
1930
- body: ['def add(x, y)',
1931
- ' x + y', 'end']),
1932
- output: "def add(x, y)\n x + y\n end\n"
1933
- },
1934
- {
1935
- input: FCB.new(title: 'foo', body: %w[bar baz]),
1936
- output: 'foo' # expect the title to remain unchanged
1937
- }
1938
- ]
1939
-
1940
- # iterate over the input and output data and
1941
- # assert that the method sets the title as expected
1942
- input_output_data.each do |data|
1943
- input = data[:input]
1944
- output = data[:output]
1945
- @mark_parse.update_title_from_body(input)
1946
- assert_equal output, input.title
1947
- end
1948
- end
1949
- end
1950
-
1951
632
  def test_select_block
1952
633
  blocks = [block1, block2]
1953
634
  menu = [m1, m2]