markdown_exec 1.3.7 → 1.3.9

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
@@ -37,6 +37,13 @@ $stdout.sync = true
37
37
 
38
38
  BLOCK_SIZE = 1024
39
39
 
40
+ # macros
41
+ #
42
+ BACK_OPTION = '* Back'
43
+ EXIT_OPTION = '* Exit'
44
+ LOAD_FILE = true
45
+ VN = 'MDE_MENU_HISTORY'
46
+
40
47
  # custom error: file specified is missing
41
48
  #
42
49
  class FileMissingError < StandardError; end
@@ -58,6 +65,18 @@ class Hash
58
65
  end
59
66
  end
60
67
 
68
+ # integer value for comparison
69
+ #
70
+ def options_fetch_display_level(options)
71
+ options.fetch(:display_level, 1)
72
+ end
73
+
74
+ # integer value for comparison
75
+ #
76
+ def options_fetch_display_level_xbase_prefix(options)
77
+ options.fetch(:level_xbase_prefix, '')
78
+ end
79
+
61
80
  # stdout manager
62
81
  #
63
82
  module FOUT
@@ -77,7 +96,7 @@ module FOUT
77
96
  end
78
97
 
79
98
  def approved_fout?(level)
80
- level <= @options[:display_level]
99
+ level <= options_fetch_display_level(@options)
81
100
  end
82
101
 
83
102
  # display output at level or lower than filter (DISPLAY_LEVEL_DEFAULT)
@@ -85,7 +104,7 @@ module FOUT
85
104
  def lout(str, level: DISPLAY_LEVEL_BASE)
86
105
  return unless approved_fout? level
87
106
 
88
- fout level == DISPLAY_LEVEL_BASE ? str : @options[:display_level_xbase_prefix] + str
107
+ fout level == DISPLAY_LEVEL_BASE ? str : options_fetch_display_level_xbase_prefix(@options) + str
89
108
  end
90
109
  end
91
110
 
@@ -121,6 +140,13 @@ module MarkdownExec
121
140
  FNR11 = '/'
122
141
  FNR12 = ',~'
123
142
 
143
+ SHELL_COLOR_OPTIONS = {
144
+ 'bash' => :menu_bash_color,
145
+ BLOCK_TYPE_LINK => :menu_link_color,
146
+ 'opts' => :menu_opts_color,
147
+ 'vars' => :menu_vars_color
148
+ }.freeze
149
+
124
150
  ##
125
151
  #
126
152
  # rubocop:disable Layout/LineLength
@@ -162,17 +188,6 @@ module MarkdownExec
162
188
  end
163
189
  end
164
190
 
165
- # return arguments after `--`
166
- #
167
- def arguments_for_child(argv = ARGV)
168
- case ind = argv.find_index('--')
169
- when nil, argv.count - 1
170
- []
171
- else
172
- argv[ind + 1..-1]
173
- end
174
- end
175
-
176
191
  ##
177
192
  # options necessary to start, parse input, defaults for cli options
178
193
  #
@@ -199,115 +214,179 @@ module MarkdownExec
199
214
  }
200
215
  end
201
216
 
217
+ # Execute a code block after approval and provide user interaction options.
218
+ #
219
+ # This method displays required code blocks, asks for user approval, and
220
+ # executes the code block if approved. It also allows users to copy the
221
+ # code to the clipboard or save it to a file.
222
+ #
223
+ # @param opts [Hash] Options hash containing configuration settings.
224
+ # @param mdoc [YourMDocClass] An instance of the MDoc class.
225
+ # @return [String] The name of the executed code block.
226
+ #
202
227
  def approve_and_execute_block(opts, mdoc)
203
- required_blocks = mdoc.collect_recursively_required_code(opts[:block_name])
204
- if opts[:output_script] || opts[:user_must_approve]
205
- display_required_code(opts,
206
- required_blocks)
228
+ selected = mdoc.get_block_by_name(opts[:block_name])
229
+ if selected[:shell] == BLOCK_TYPE_LINK
230
+ handle_link_shell(opts, selected)
231
+ elsif selected[:shell] == 'opts'
232
+ handle_opts_shell(opts, selected)
233
+ else
234
+ required_lines = collect_required_code_blocks(opts, mdoc, selected)
235
+ # Display required code blocks if requested or required approval.
236
+ if opts[:output_script] || opts[:user_must_approve]
237
+ display_required_code(opts, required_lines)
238
+ end
239
+
240
+ allow = true
241
+ allow = user_approval(opts, required_lines) if opts[:user_must_approve]
242
+ opts[:ir_approve] = allow
243
+ mdoc.get_block_by_name(opts[:block_name])
244
+ execute_approved_block(opts, required_lines) if opts[:ir_approve]
245
+
246
+ [!LOAD_FILE, '']
207
247
  end
248
+ end
208
249
 
209
- allow = true
210
- if opts[:user_must_approve]
211
- loop do
212
- (sel = @prompt.select(opts[:prompt_approve_block],
213
- filter: true) do |menu|
214
- menu.default 1
215
- menu.choice opts[:prompt_yes], 1
216
- menu.choice opts[:prompt_no], 2
217
- menu.choice opts[:prompt_script_to_clipboard], 3
218
- menu.choice opts[:prompt_save_script], 4
219
- end)
220
- allow = (sel == 1)
221
- if sel == 3
222
- text = required_blocks.flatten.join($INPUT_RECORD_SEPARATOR)
223
- Clipboard.copy(text)
224
- fout "Clipboard updated: #{required_blocks.count} blocks," /
225
- " #{required_blocks.flatten.count} lines," /
226
- " #{text.length} characters"
227
- end
228
- if sel == 4
229
- write_command_file(opts.merge(save_executed_script: true),
230
- required_blocks)
231
- fout "File saved: #{@options[:saved_filespec]}"
232
- end
233
- break if [1, 2].include? sel
234
- end
250
+ def handle_link_shell(opts, selected)
251
+ data = YAML.load(selected[:body].join("\n"))
252
+
253
+ # add to front of history
254
+ #
255
+ ENV[VN] = opts[:filename] + opts[:history_document_separator] + ENV.fetch(VN, '')
256
+
257
+ opts[:filename] = data.fetch('file', nil)
258
+ return !LOAD_FILE unless opts[:filename]
259
+
260
+ data.fetch('vars', []).each do |var|
261
+ ENV[var[0]] = var[1].to_s
235
262
  end
236
- (opts[:ir_approve] = allow)
237
263
 
238
- selected = mdoc.get_block_by_name opts[:block_name]
264
+ [LOAD_FILE, data.fetch('block', '')]
265
+ end
239
266
 
240
- if opts[:ir_approve]
241
- write_command_file opts, required_blocks
242
- command_execute opts, required_blocks.flatten.join("\n")
243
- save_execution_output
244
- output_execution_summary
245
- output_execution_result
267
+ def handle_opts_shell(opts, selected)
268
+ data = YAML.load(selected[:body].join("\n"))
269
+ data.each_key do |key|
270
+ opts[key.to_sym] = value = data[key].to_s
271
+ next unless opts[:menu_opts_set_format].present?
272
+
273
+ print format(
274
+ opts[:menu_opts_set_format],
275
+ { key: key,
276
+ value: value }
277
+ ).send(opts[:menu_opts_set_color].to_sym)
278
+ end
279
+ [!LOAD_FILE, '']
280
+ end
281
+
282
+ def user_approval(opts, required_lines)
283
+ # Present a selection menu for user approval.
284
+ sel = @prompt.select(opts[:prompt_approve_block], filter: true) do |menu|
285
+ menu.default 1
286
+ menu.choice opts[:prompt_yes], 1
287
+ menu.choice opts[:prompt_no], 2
288
+ menu.choice opts[:prompt_script_to_clipboard], 3
289
+ menu.choice opts[:prompt_save_script], 4
290
+ end
291
+
292
+ if sel == 3
293
+ copy_to_clipboard(required_lines)
294
+ elsif sel == 4
295
+ save_to_file(opts, required_lines)
246
296
  end
247
297
 
248
- selected[:name]
298
+ sel == 1
299
+ end
300
+
301
+ def execute_approved_block(opts, required_lines)
302
+ write_command_file(opts, required_lines)
303
+ command_execute(
304
+ opts,
305
+ required_lines.flatten.join("\n"),
306
+ args: opts.fetch(:pass_args, [])
307
+ )
308
+ save_execution_output
309
+ output_execution_summary
310
+ output_execution_result
311
+ end
312
+
313
+ # Collect required code blocks based on the provided options.
314
+ #
315
+ # @param opts [Hash] Options hash containing configuration settings.
316
+ # @param mdoc [YourMDocClass] An instance of the MDoc class.
317
+ # @return [Array<String>] Required code blocks as an array of lines.
318
+ def collect_required_code_blocks(opts, mdoc, selected)
319
+ required = mdoc.collect_recursively_required_code(opts[:block_name])
320
+ required_lines = required[:code]
321
+ required[:blocks]
322
+
323
+ # Apply hash in opts block to environment variables
324
+ if selected[:shell] == BLOCK_TYPE_VARS
325
+ data = YAML.load(selected[:body].join("\n"))
326
+ data.each_key do |key|
327
+ ENV[key] = value = data[key].to_s
328
+ next unless opts[:menu_vars_set_format].present?
329
+
330
+ print format(
331
+ opts[:menu_vars_set_format],
332
+ { key: key,
333
+ value: value }
334
+ ).send(opts[:menu_vars_set_color].to_sym)
335
+ end
336
+ end
337
+
338
+ required_lines
249
339
  end
250
340
 
251
341
  def cfile
252
342
  @cfile ||= CachedNestedFileReader.new(import_pattern: @options.fetch(:import_pattern))
253
343
  end
254
344
 
255
- # :reek:DuplicateMethodCall
256
- # :reek:UncommunicativeVariableName { exclude: [ e ] }
257
- # :reek:LongYieldList
258
- def command_execute(opts, command)
259
- #d 'execute command and yield outputs'
345
+ EF_STDOUT = :stdout
346
+ EF_STDERR = :stderr
347
+ EF_STDIN = :stdin
348
+
349
+ # Handles reading and processing lines from a given IO stream
350
+ #
351
+ # @param stream [IO] The IO stream to read from (e.g., stdout, stderr, stdin).
352
+ # @param file_type [Symbol] The type of file to which the stream corresponds.
353
+ def handle_stream(opts, stream, file_type, swap: false)
354
+ Thread.new do
355
+ until (line = stream.gets).nil?
356
+ @execute_files[file_type] = @execute_files[file_type] + [line.strip]
357
+ print line if opts[:output_stdout]
358
+ yield line if block_given?
359
+ end
360
+ rescue IOError
361
+ #d 'stdout IOError, thread killed, do nothing'
362
+ end
363
+ end
364
+
365
+ # Existing command_execute method
366
+ def command_execute(opts, command, args: [])
260
367
  @execute_files = Hash.new([])
261
368
  @execute_options = opts
262
369
  @execute_started_at = Time.now.utc
263
370
 
264
- args = []
265
- Open3.popen3(@options[:shell], '-c',
266
- command, ARGV[0], *args) do |stdin, stdout, stderr, exec_thr|
267
- #d 'command started'
268
- Thread.new do
269
- until (line = stdout.gets).nil?
270
- @execute_files[EF_STDOUT] = @execute_files[EF_STDOUT] + [line]
271
- print line if opts[:output_stdout]
272
- yield nil, line, nil, exec_thr if block_given?
273
- end
274
- rescue IOError
275
- #d 'stdout IOError, thread killed, do nothing'
371
+ Open3.popen3(opts[:shell], '-c', command, opts[:filename],
372
+ *args) do |stdin, stdout, stderr, exec_thr|
373
+ handle_stream(opts, stdout, EF_STDOUT) do |line|
374
+ yield nil, line, nil, exec_thr if block_given?
276
375
  end
277
-
278
- Thread.new do
279
- until (line = stderr.gets).nil?
280
- @execute_files[EF_STDERR] = @execute_files[EF_STDERR] + [line]
281
- print line if opts[:output_stdout]
282
- yield nil, nil, line, exec_thr if block_given?
283
- end
284
- rescue IOError
285
- #d 'stderr IOError, thread killed, do nothing'
376
+ handle_stream(opts, stderr, EF_STDERR) do |line|
377
+ yield nil, nil, line, exec_thr if block_given?
286
378
  end
287
379
 
288
- in_thr = Thread.new do
289
- while exec_thr.alive? # reading input until the child process ends
290
- stdin.puts(line = $stdin.gets)
291
- @execute_files[EF_STDIN] = @execute_files[EF_STDIN] + [line]
292
- yield line, nil, nil, exec_thr if block_given?
293
- end
294
- #d 'exec_thr now dead'
295
- rescue StandardError
296
- #d 'stdin error, thread killed, do nothing'
380
+ in_thr = handle_stream(opts, $stdin, EF_STDIN) do |line|
381
+ stdin.puts(line)
382
+ yield line, nil, nil, exec_thr if block_given?
297
383
  end
298
384
 
299
- #d 'join exec_thr'
300
385
  exec_thr.join
301
-
302
- #d 'wait before closing stdin'
303
386
  sleep 0.1
304
-
305
- #d 'kill stdin thread'
306
- in_thr.kill
307
- # @return_code = exec_thr.value
308
- #d 'command end'
387
+ in_thr.kill if in_thr&.alive?
309
388
  end
310
- #d 'command completed'
389
+
311
390
  @execute_completed_at = Time.now.utc
312
391
  rescue Errno::ENOENT => err
313
392
  #d 'command error ENOENT triggered by missing command in script'
@@ -326,19 +405,19 @@ module MarkdownExec
326
405
  end
327
406
 
328
407
  def count_blocks_in_filename
329
- fenced_start_and_end_match = Regexp.new @options[:fenced_start_and_end_match]
408
+ fenced_start_and_end_regex = Regexp.new @options[:fenced_start_and_end_regex]
330
409
  cnt = 0
331
410
  cfile.readlines(@options[:filename]).each do |line|
332
- cnt += 1 if line.match(fenced_start_and_end_match)
411
+ cnt += 1 if line.match(fenced_start_and_end_regex)
333
412
  end
334
413
  cnt / 2
335
414
  end
336
415
 
337
416
  # :reek:DuplicateMethodCall
338
- def display_required_code(opts, required_blocks)
417
+ def display_required_code(opts, required_lines)
339
418
  frame = opts[:output_divider].send(opts[:output_divider_color].to_sym)
340
419
  fout frame
341
- required_blocks.each { |cb| fout cb }
420
+ required_lines.each { |cb| fout cb }
342
421
  fout frame
343
422
  end
344
423
 
@@ -408,7 +487,6 @@ module MarkdownExec
408
487
  def get_block_summary(call_options, fcb)
409
488
  opts = optsmerge call_options
410
489
  # return fcb.body unless opts[:struct]
411
-
412
490
  return fcb unless opts[:bash]
413
491
 
414
492
  fcb.call = fcb.title.match(Regexp.new(opts[:block_calls_scan]))&.fetch(1, nil)
@@ -420,16 +498,23 @@ module MarkdownExec
420
498
  bm = option_match_groups(titlexcall, opts[:block_name_match])
421
499
  fcb.stdin = option_match_groups(titlexcall, opts[:block_stdin_scan])
422
500
  fcb.stdout = option_match_groups(titlexcall, opts[:block_stdout_scan])
423
- fcb.title = fcb.name = (bm && bm[1] ? bm[:title] : titlexcall)
501
+
502
+ shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
503
+ fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall
504
+ fcb.dname = if shell_color_option && opts[shell_color_option].present?
505
+ fcb.oname.send(opts[shell_color_option].to_sym)
506
+ else
507
+ fcb.oname
508
+ end
424
509
  fcb
425
510
  end
426
511
 
427
512
  # :reek:DuplicateMethodCall
428
513
  # :reek:LongYieldList
429
514
  # :reek:NestedIterators
430
- def iter_blocks_in_file(opts = {})
431
- # opts = optsmerge call_options, options_block
515
+ #---
432
516
 
517
+ def iter_blocks_in_file(opts = {}, &block)
433
518
  unless opts[:filename]&.present?
434
519
  fout 'No blocks found.'
435
520
  return
@@ -440,10 +525,10 @@ module MarkdownExec
440
525
  return
441
526
  end
442
527
 
443
- fenced_start_and_end_match = Regexp.new opts[:fenced_start_and_end_match]
444
- fenced_start_ex = Regexp.new opts[:fenced_start_ex_match]
528
+ fenced_start_and_end_regex = Regexp.new opts[:fenced_start_and_end_regex]
529
+ fenced_start_extended_regex = Regexp.new opts[:fenced_start_extended_regex]
445
530
  fcb = FCB.new
446
- in_block = false
531
+ in_fenced_block = false
447
532
  headings = []
448
533
 
449
534
  ## get type of messages to select
@@ -452,70 +537,105 @@ module MarkdownExec
452
537
 
453
538
  cfile.readlines(opts[:filename]).each.with_index do |line, _line_num|
454
539
  continue unless line
540
+ headings = update_headings(line, headings, opts) if opts[:menu_blocks_with_headings]
455
541
 
456
- if opts[:menu_blocks_with_headings]
457
- if (lm = line.match(Regexp.new(opts[:heading3_match])))
458
- headings = [headings[0], headings[1], lm[:name]]
459
- elsif (lm = line.match(Regexp.new(opts[:heading2_match])))
460
- headings = [headings[0], lm[:name]]
461
- elsif (lm = line.match(Regexp.new(opts[:heading1_match])))
462
- headings = [lm[:name]]
463
- end
464
- end
465
-
466
- if line.match(fenced_start_and_end_match)
467
- if in_block
468
- # end fcb
469
- #
470
- fcb.name = fcb.title || ''
471
- if fcb.body
472
- if fcb.title.nil? || fcb.title.empty?
473
- fcb.title = fcb.body.join(' ').gsub(/ +/, ' ')[0..64]
474
- end
475
-
476
- if block_given? && selected_messages.include?(:blocks) &&
477
- Filter.fcb_select?(opts, fcb)
478
- yield :blocks, fcb
479
- end
480
- end
481
- in_block = false
542
+ if line.match(fenced_start_and_end_regex)
543
+ if in_fenced_block
544
+ process_fenced_block(fcb, opts, selected_messages, &block)
545
+ in_fenced_block = false
482
546
  else
483
- # start fcb
484
- #
485
- in_block = true
486
-
487
- fcb_title_groups = line.match(fenced_start_ex).named_captures.sym_keys
488
- fcb = FCB.new
489
- fcb.headings = headings
490
- fcb.name = fcb_title_groups.fetch(:name, '')
491
- fcb.shell = fcb_title_groups.fetch(:shell, '')
492
- fcb.title = fcb_title_groups.fetch(:name, '')
493
-
494
- # selected fcb
495
- #
496
- fcb.body = []
497
-
498
- rest = fcb_title_groups.fetch(:rest, '')
499
- fcb.reqs = rest.scan(/\+[^\s]+/).map { |req| req[1..-1] }
500
-
501
- fcb.call = rest.match(Regexp.new(opts[:block_calls_scan]))&.to_a&.first
502
- fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
503
- tn.named_captures.sym_keys
504
- end
505
- fcb.stdout = if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
506
- tn.named_captures.sym_keys
507
- end
547
+ fcb = start_fenced_block(opts, line, headings, fenced_start_extended_regex)
548
+ in_fenced_block = true
508
549
  end
509
- elsif in_block && fcb.body
550
+ elsif in_fenced_block && fcb.body
510
551
  dp 'append line to fcb body'
511
552
  fcb.body += [line.chomp]
512
- elsif block_given? && selected_messages.include?(:line)
513
- dp 'text outside of fcb'
514
- fcb = FCB.new
515
- fcb.body = [line]
516
- yield :line, fcb
553
+ else
554
+ process_line(line, opts, selected_messages, &block)
555
+ end
556
+ end
557
+ end
558
+
559
+ def start_fenced_block(opts, line, headings, fenced_start_extended_regex)
560
+ fcb_title_groups = line.match(fenced_start_extended_regex).named_captures.sym_keys
561
+ fcb = FCB.new
562
+ fcb.headings = headings
563
+ fcb.oname = fcb.dname = fcb_title_groups.fetch(:name, '')
564
+ fcb.shell = fcb_title_groups.fetch(:shell, '')
565
+ fcb.title = fcb_title_groups.fetch(:name, '')
566
+
567
+ # selected fcb
568
+ fcb.body = []
569
+
570
+ rest = fcb_title_groups.fetch(:rest, '')
571
+ fcb.reqs, fcb.wraps =
572
+ split_array(rest.scan(/\+[^\s]+/).map { |req| req[1..-1] }) do |name|
573
+ !name.match(Regexp.new(opts[:block_name_wrapper_match]))
574
+ end
575
+ fcb.call = rest.match(Regexp.new(opts[:block_calls_scan]))&.to_a&.first
576
+ fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
577
+ tn.named_captures.sym_keys
578
+ end
579
+ fcb.stdout = if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
580
+ tn.named_captures.sym_keys
581
+ end
582
+ fcb
583
+ end
584
+
585
+ def process_fenced_block(fcb, opts, selected_messages, &block)
586
+ fcb.oname = fcb.dname = fcb.title || ''
587
+ return unless fcb.body
588
+
589
+ set_fcb_title(fcb)
590
+
591
+ if block &&
592
+ selected_messages.include?(:blocks) &&
593
+ Filter.fcb_select?(opts, fcb)
594
+ block.call :blocks, fcb
595
+ end
596
+ end
597
+
598
+ def process_line(line, _opts, selected_messages, &block)
599
+ return unless block && selected_messages.include?(:line)
600
+
601
+ # dp 'text outside of fcb'
602
+ fcb = FCB.new
603
+ fcb.body = [line]
604
+ block.call(:line, fcb)
605
+ end
606
+
607
+ # set the title of an FCB object based on its body if it is nil or empty
608
+ def set_fcb_title(fcb)
609
+ return unless fcb.title.nil? || fcb.title.empty?
610
+
611
+ fcb.title = (fcb&.body || []).join(' ').gsub(/ +/, ' ')[0..64]
612
+ end
613
+
614
+ def split_array(arr)
615
+ true_list = []
616
+ false_list = []
617
+
618
+ arr.each do |element|
619
+ if yield(element)
620
+ true_list << element
621
+ else
622
+ false_list << element
517
623
  end
518
624
  end
625
+
626
+ [true_list, false_list]
627
+ end
628
+
629
+ def update_headings(line, headings, opts)
630
+ if (lm = line.match(Regexp.new(opts[:heading3_match])))
631
+ [headings[0], headings[1], lm[:name]]
632
+ elsif (lm = line.match(Regexp.new(opts[:heading2_match])))
633
+ [headings[0], lm[:name]]
634
+ elsif (lm = line.match(Regexp.new(opts[:heading1_match])))
635
+ [lm[:name]]
636
+ else
637
+ headings
638
+ end
519
639
  end
520
640
 
521
641
  # return body, title if option.struct
@@ -530,10 +650,11 @@ module MarkdownExec
530
650
  blocks.push FCB.new({
531
651
  # name: '',
532
652
  chrome: true,
533
- name: format(
653
+ dname: format(
534
654
  opts[:menu_divider_format],
535
655
  opts[:menu_initial_divider]
536
656
  ).send(opts[:menu_divider_color].to_sym),
657
+ oname: opts[:menu_initial_divider],
537
658
  disabled: '' # __LINE__.to_s
538
659
  })
539
660
  end
@@ -554,8 +675,9 @@ module MarkdownExec
554
675
  blocks.push FCB.new(
555
676
  { chrome: true,
556
677
  disabled: '',
557
- name: format(opts[:menu_divider_format],
558
- mbody[:name]).send(opts[:menu_divider_color].to_sym) }
678
+ dname: format(opts[:menu_divider_format],
679
+ mbody[:name]).send(opts[:menu_divider_color].to_sym),
680
+ oname: mbody[:name] }
559
681
  )
560
682
  end
561
683
  elsif opts[:menu_task_match].present? &&
@@ -564,10 +686,14 @@ module MarkdownExec
564
686
  blocks.push FCB.new(
565
687
  { chrome: true,
566
688
  disabled: '',
567
- name: format(
689
+ dname: format(
690
+ opts[:menu_task_format],
691
+ $~.named_captures.transform_keys(&:to_sym)
692
+ ).send(opts[:menu_task_color].to_sym),
693
+ oname: format(
568
694
  opts[:menu_task_format],
569
695
  $~.named_captures.transform_keys(&:to_sym)
570
- ).send(opts[:menu_task_color].to_sym) }
696
+ ) }
571
697
  )
572
698
  end
573
699
  else
@@ -584,9 +710,10 @@ module MarkdownExec
584
710
  blocks.push FCB.new(
585
711
  { chrome: true,
586
712
  disabled: '',
587
- name: format(opts[:menu_divider_format],
588
- opts[:menu_final_divider])
589
- .send(opts[:menu_divider_color].to_sym) }
713
+ dname: format(opts[:menu_divider_format],
714
+ opts[:menu_final_divider])
715
+ .send(opts[:menu_divider_color].to_sym),
716
+ oname: opts[:menu_final_divider] }
590
717
  )
591
718
  end
592
719
  blocks
@@ -667,7 +794,7 @@ module MarkdownExec
667
794
  else
668
795
  # blocks.map(&:name)
669
796
  blocks.map do |block|
670
- block.fetch(:text, nil) || block.fetch(:name, nil)
797
+ block.fetch(:text, nil) || block.oname
671
798
  end
672
799
  end.compact.reject(&:empty?)
673
800
  end
@@ -760,10 +887,10 @@ module MarkdownExec
760
887
  when :line
761
888
  if options[:menu_divider_match] &&
762
889
  (mbody = fcb.body[0].match(options[:menu_divider_match]))
763
- menu.push FCB.new({ name: mbody[:name], disabled: '' })
890
+ menu.push FCB.new({ dname: mbody[:name], oname: mbody[:name], disabled: '' })
764
891
  end
765
892
  when :blocks
766
- menu += [fcb.name]
893
+ menu += [fcb.oname]
767
894
  end
768
895
  end
769
896
  menu
@@ -809,7 +936,7 @@ module MarkdownExec
809
936
  def options_finalize(rest)
810
937
  ## position 0: file or folder (optional)
811
938
  #
812
- if (pos = rest.fetch(0, nil))&.present?
939
+ if (pos = rest.shift)&.present?
813
940
  if Dir.exist?(pos)
814
941
  @options[:path] = pos
815
942
  elsif File.exist?(pos)
@@ -821,7 +948,7 @@ module MarkdownExec
821
948
 
822
949
  ## position 1: block name (optional)
823
950
  #
824
- block_name = rest.fetch(1, nil)
951
+ block_name = rest.shift
825
952
  @options[:block_name] = block_name if block_name.present?
826
953
  end
827
954
 
@@ -868,13 +995,31 @@ module MarkdownExec
868
995
  }
869
996
  end
870
997
 
998
+ ## insert back option at head or tail
999
+ #
1000
+ ## Adds a back option at the head or tail of a menu
1001
+ #
1002
+ def prompt_menu_add_back(items, label = BACK_OPTION)
1003
+ return items unless @options[:menu_with_back]
1004
+
1005
+ history = ENV.fetch('MDE_MENU_HISTORY', '')
1006
+ return items unless history.present?
1007
+
1008
+ @hs_curr, @hs_rest = split_string_on_first_char(
1009
+ history,
1010
+ @options[:history_document_separator]
1011
+ )
1012
+
1013
+ @options[:menu_back_at_top] ? [label] + items : items + [label]
1014
+ end
1015
+
871
1016
  ## insert exit option at head or tail
872
1017
  #
873
- def prompt_menu_add_exit(_prompt_text, items, exit_option, _opts = {})
1018
+ def prompt_menu_add_exit(items, label = EXIT_OPTION)
874
1019
  if @options[:menu_exit_at_top]
875
- (@options[:menu_with_exit] ? [exit_option] : []) + items
1020
+ (@options[:menu_with_exit] ? [label] : []) + items
876
1021
  else
877
- items + (@options[:menu_with_exit] ? [exit_option] : [])
1022
+ items + (@options[:menu_with_exit] ? [label] : [])
878
1023
  end
879
1024
  end
880
1025
 
@@ -883,10 +1028,31 @@ module MarkdownExec
883
1028
  # return selected option or nil
884
1029
  #
885
1030
  def prompt_with_quit(prompt_text, items, opts = {})
886
- exit_option = '* Exit'
887
- sel = @prompt.select(prompt_text, prompt_menu_add_exit(prompt_text, items, exit_option, opts),
1031
+ obj = prompt_with_quit2(prompt_text, items, opts)
1032
+ if obj.fetch(:option, nil)
1033
+ nil
1034
+ else
1035
+ obj[:selected]
1036
+ end
1037
+ end
1038
+
1039
+ ## tty prompt to select
1040
+ # insert exit option at head or tail
1041
+ # return option:, selected option:
1042
+ #
1043
+ def prompt_with_quit2(prompt_text, items, opts = {})
1044
+ sel = @prompt.select(prompt_text,
1045
+ prompt_menu_add_exit(
1046
+ prompt_menu_add_back(items)
1047
+ ),
888
1048
  opts.merge(filter: true))
889
- sel == exit_option ? nil : sel
1049
+ if sel == BACK_OPTION
1050
+ { option: sel, curr: @hs_curr, rest: @hs_rest }
1051
+ elsif sel == EXIT_OPTION
1052
+ { option: sel }
1053
+ else
1054
+ { selected: sel }
1055
+ end
890
1056
  end
891
1057
 
892
1058
  # :reek:UtilityFunction ### temp
@@ -899,12 +1065,8 @@ module MarkdownExec
899
1065
 
900
1066
  # :reek:NestedIterators
901
1067
  def run
902
- ## default configuration
903
- #
904
1068
  @options = base_options
905
1069
 
906
- ## read local configuration file
907
- #
908
1070
  read_configuration_file! @options,
909
1071
  ".#{MarkdownExec::APP_NAME.downcase}.yml"
910
1072
 
@@ -920,12 +1082,13 @@ module MarkdownExec
920
1082
  menu_option_append opts, options, item
921
1083
  end
922
1084
  end
923
- option_parser.load # filename defaults to basename of the program
924
- # without suffix in a directory ~/.options
925
- option_parser.environment # env defaults to the basename of the program
926
- # child_argv = arguments_for_child
1085
+ option_parser.load
1086
+ option_parser.environment
927
1087
  rest = option_parser.parse!(arguments_for_mde) # (into: options)
928
1088
 
1089
+ # pass through arguments excluded from OptionParser with `--`
1090
+ @options[:pass_args] = ARGV[rest.count + 1..]
1091
+
929
1092
  begin
930
1093
  options_finalize rest
931
1094
  exec_block options, options[:block_name]
@@ -984,54 +1147,98 @@ module MarkdownExec
984
1147
  File.write(@options[:logged_stdout_filespec], ol.join)
985
1148
  end
986
1149
 
987
- def select_approve_and_execute_block(call_options, &options_block)
988
- opts = optsmerge call_options, options_block
989
- blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
990
- mdoc = MDoc.new(blocks_in_file) do |nopts|
991
- opts.merge!(nopts)
992
- end
993
- blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
1150
+ # Prepare the blocks menu by adding labels and other necessary details.
1151
+ #
1152
+ # @param blocks_in_file [Array<Hash>] The list of blocks from the file.
1153
+ # @param opts [Hash] The options hash.
1154
+ # @return [Array<Hash>] The updated blocks menu.
1155
+ def prepare_blocks_menu(blocks_in_file, opts)
1156
+ # next if fcb.fetch(:disabled, false)
1157
+ # next unless fcb.fetch(:name, '').present?
1158
+ blocks_in_file.map do |fcb|
1159
+ fcb.merge!(
1160
+ name: fcb.dname,
1161
+ label: BlockLabel.make(
1162
+ body: fcb[:body],
1163
+ filename: opts[:filename],
1164
+ headings: fcb.fetch(:headings, []),
1165
+ menu_blocks_with_docname: opts[:menu_blocks_with_docname],
1166
+ menu_blocks_with_headings: opts[:menu_blocks_with_headings],
1167
+ text: fcb[:text],
1168
+ title: fcb[:title]
1169
+ )
1170
+ )
1171
+ fcb.to_h
1172
+ end.compact
1173
+ end
994
1174
 
1175
+ # Select and execute a code block from a Markdown document.
1176
+ #
1177
+ # This method allows the user to interactively select a code block from a
1178
+ # Markdown document, obtain approval, and execute the chosen block of code.
1179
+ #
1180
+ # @param call_options [Hash] Initial options for the method.
1181
+ # @param options_block [Block] Block of options to be merged with call_options.
1182
+ # @return [Nil] Returns nil if no code block is selected or an error occurs.
1183
+ def select_approve_and_execute_block(call_options, &options_block)
1184
+ opts = optsmerge(call_options, options_block)
995
1185
  repeat_menu = true && !opts[:block_name].present?
1186
+
1187
+ load_file = !LOAD_FILE
996
1188
  loop do
997
- unless opts[:block_name].present?
998
- pt = (opts[:prompt_select_block]).to_s
999
-
1000
- bm = blocks_menu.map do |fcb|
1001
- # next if fcb.fetch(:disabled, false)
1002
- # next unless fcb.fetch(:name, '').present?
1003
-
1004
- fcb.merge!(
1005
- label: BlockLabel.make(
1006
- body: fcb[:body],
1007
- filename: opts[:filename],
1008
- headings: fcb.fetch(:headings, []),
1009
- menu_blocks_with_docname: opts[:menu_blocks_with_docname],
1010
- menu_blocks_with_headings: opts[:menu_blocks_with_headings],
1011
- text: fcb[:text],
1012
- title: fcb[:title]
1013
- )
1014
- )
1189
+ # load file
1190
+ #
1191
+ loop do
1192
+ # repeat menu
1193
+ #
1194
+ load_file = !LOAD_FILE
1195
+ blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
1196
+ mdoc = MDoc.new(blocks_in_file) do |nopts|
1197
+ opts.merge!(nopts)
1198
+ end
1199
+ blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
1200
+ unless opts[:block_name].present?
1201
+ pt = opts[:prompt_select_block].to_s
1202
+ bm = prepare_blocks_menu(blocks_menu, opts)
1203
+ return nil if bm.count.zero?
1204
+
1205
+ # sel = prompt_with_quit(pt, bm, per_page: opts[:select_page_height])
1206
+ # return nil if sel.nil?
1207
+ obj = prompt_with_quit2(pt, bm, per_page: opts[:select_page_height])
1208
+ case obj.fetch(:option, nil)
1209
+ when EXIT_OPTION
1210
+ return nil
1211
+ when BACK_OPTION
1212
+ opts[:filename] = obj[:curr]
1213
+ opts[:block_name] = @options[:block_name] = ''
1214
+ ENV['MDE_MENU_HISTORY'] = obj[:rest]
1215
+ load_file = LOAD_FILE # later: exit menu, load file
1216
+ else
1217
+ sel = obj[:selected]
1218
+
1219
+ ## store selected option
1220
+ #
1221
+ label_block = blocks_in_file.select do |fcb|
1222
+ fcb.dname == sel
1223
+ end.fetch(0, nil)
1224
+ opts[:block_name] = @options[:block_name] = label_block.oname
1225
+ end
1226
+ end
1227
+ break if load_file == LOAD_FILE
1015
1228
 
1016
- fcb.to_h
1017
- end.compact
1018
- return nil if bm.count.zero?
1229
+ # later: load file
1019
1230
 
1020
- sel = prompt_with_quit pt, bm,
1021
- per_page: opts[:select_page_height]
1022
- return nil if sel.nil?
1231
+ load_file, block_name = approve_and_execute_block(opts, mdoc)
1023
1232
 
1024
- ## store selected option
1025
- #
1026
- label_block = blocks_in_file.select do |fcb|
1027
- fcb[:label] == sel
1028
- end.fetch(0, nil)
1029
- opts[:block_name] = @options[:block_name] = label_block.fetch(:name, '')
1030
- end
1031
- approve_and_execute_block opts, mdoc
1032
- break unless repeat_menu
1233
+ opts[:block_name] = block_name
1234
+ if load_file == LOAD_FILE
1235
+ repeat_menu = true
1236
+ break
1237
+ end
1033
1238
 
1034
- opts[:block_name] = ''
1239
+ break unless repeat_menu
1240
+ end
1241
+ break if load_file != LOAD_FILE
1035
1242
  end
1036
1243
  rescue StandardError => err
1037
1244
  warn(error = "ERROR ** MarkParse.select_approve_and_execute_block(); #{err.inspect}")
@@ -1093,6 +1300,22 @@ module MarkdownExec
1093
1300
  end.to_yaml
1094
1301
  end
1095
1302
 
1303
+ # Splits the given string on the first occurrence of the specified character.
1304
+ # Returns an array containing the portion of the string before the character and the rest of the string.
1305
+ #
1306
+ # @param input_str [String] The string to be split.
1307
+ # @param split_char [String] The character on which to split the string.
1308
+ # @return [Array<String>] An array containing two elements: the part of the string before split_char, and the rest of the string.
1309
+ def split_string_on_first_char(input_str, split_char)
1310
+ split_index = input_str.index(split_char)
1311
+
1312
+ if split_index.nil?
1313
+ [input_str, '']
1314
+ else
1315
+ [input_str[0...split_index], input_str[(split_index + 1)..-1]]
1316
+ end
1317
+ end
1318
+
1096
1319
  def tab_completions(data = menu_for_optparse)
1097
1320
  data.map do |item|
1098
1321
  "--#{item[:long_name]}" if item[:long_name]
@@ -1110,7 +1333,7 @@ module MarkdownExec
1110
1333
  @options
1111
1334
  end
1112
1335
 
1113
- def write_command_file(call_options, required_blocks)
1336
+ def write_command_file(call_options, required_lines)
1114
1337
  return unless call_options[:save_executed_script]
1115
1338
 
1116
1339
  time_now = Time.now.utc
@@ -1137,7 +1360,7 @@ module MarkdownExec
1137
1360
  "# file_name: #{opts[:filename]}\n" \
1138
1361
  "# block_name: #{opts[:block_name]}\n" \
1139
1362
  "# time: #{time_now}\n" \
1140
- "#{required_blocks.flatten.join("\n")}\n")
1363
+ "#{required_lines.flatten.join("\n")}\n")
1141
1364
  return if @options[:saved_script_chmod].zero?
1142
1365
 
1143
1366
  File.chmod @options[:saved_script_chmod], @options[:saved_filespec]
@@ -1145,4 +1368,69 @@ module MarkdownExec
1145
1368
  end # class MarkParse
1146
1369
  end # module MarkdownExec
1147
1370
 
1148
- require 'minitest/autorun' if $PROGRAM_NAME == __FILE__
1371
+ if $PROGRAM_NAME == __FILE__
1372
+ require 'bundler/setup'
1373
+ Bundler.require(:default)
1374
+
1375
+ require 'minitest/autorun'
1376
+
1377
+ module MarkdownExec
1378
+ class TestMarkParse < Minitest::Test
1379
+ require 'mocha/minitest'
1380
+
1381
+ def test_calling_execute_approved_block_calls_command_execute_with_argument_args_value
1382
+ pigeon = 'E'
1383
+ obj = { pass_args: pigeon }
1384
+
1385
+ c = MarkdownExec::MarkParse.new
1386
+
1387
+ # Expect that method command_execute is called with argument args having value pigeon
1388
+ c.expects(:command_execute).with(
1389
+ obj,
1390
+ '',
1391
+ args: pigeon)
1392
+
1393
+ # Call method execute_approved_block
1394
+ c.execute_approved_block(obj, [])
1395
+ end
1396
+
1397
+ def setup
1398
+ @mark_parse = MarkdownExec::MarkParse.new
1399
+ end
1400
+
1401
+ def test_set_fcb_title
1402
+ # sample input and output data for testing set_fcb_title method
1403
+ input_output_data = [
1404
+ {
1405
+ input: FCB.new(title: nil, body: ["puts 'Hello, world!'"]),
1406
+ output: "puts 'Hello, world!'"
1407
+ },
1408
+ {
1409
+ input: FCB.new(title: '', body: ['def add(x, y)', ' x + y', 'end']),
1410
+ output: 'def add(x, y) x + y end'
1411
+ },
1412
+ {
1413
+ input: FCB.new(title: 'foo', body: %w[bar baz]),
1414
+ output: 'foo' # expect the title to remain unchanged
1415
+ }
1416
+ ]
1417
+
1418
+ # iterate over the input and output data and assert that the method sets the title as expected
1419
+ input_output_data.each do |data|
1420
+ input = data[:input]
1421
+ output = data[:output]
1422
+ @mark_parse.set_fcb_title(input)
1423
+ assert_equal output, input.title
1424
+ end
1425
+ end
1426
+ end
1427
+
1428
+ ###
1429
+
1430
+ # result = split_string_on_first_char("hello-world", "-")
1431
+ # puts result.inspect # Output should be ["hello", "world"]
1432
+
1433
+ # result = split_string_on_first_char("hello", "-")
1434
+ # puts result.inspect # Output should be ["hello", ""]
1435
+ end
1436
+ end