markdown_exec 1.4.1 → 1.6
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -1
- data/CHANGELOG.md +22 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +6 -1
- data/bin/bmde +103 -1
- data/bin/tab_completion.sh +2 -2
- data/examples/opts.md +10 -1
- data/lib/block_types.rb +9 -5
- data/lib/filter.rb +15 -4
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/markdown_exec.rb +618 -361
- data/lib/mdoc.rb +106 -32
- data/lib/menu.src.yml +179 -8
- data/lib/menu.yml +65 -9
- data/lib/pty1.rb +26 -0
- metadata +5 -4
data/lib/markdown_exec.rb
CHANGED
@@ -38,10 +38,6 @@ $stdout.sync = true
|
|
38
38
|
|
39
39
|
MDE_HISTORY_ENV_NAME = 'MDE_MENU_HISTORY'
|
40
40
|
|
41
|
-
# macros
|
42
|
-
#
|
43
|
-
LOAD_FILE = true
|
44
|
-
|
45
41
|
# custom error: file specified is missing
|
46
42
|
#
|
47
43
|
class FileMissingError < StandardError; end
|
@@ -63,6 +59,17 @@ class Hash
|
|
63
59
|
end
|
64
60
|
end
|
65
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
|
+
|
66
73
|
# integer value for comparison
|
67
74
|
#
|
68
75
|
def options_fetch_display_level(options)
|
@@ -110,16 +117,23 @@ def dp(str)
|
|
110
117
|
lout " => #{str}", level: DISPLAY_LEVEL_DEBUG
|
111
118
|
end
|
112
119
|
|
120
|
+
def rpry
|
121
|
+
require 'pry-nav'
|
122
|
+
require 'pry-stack_explorer'
|
123
|
+
end
|
124
|
+
|
113
125
|
public
|
114
126
|
|
115
127
|
# :reek:UtilityFunction
|
116
|
-
def list_recent_output(saved_stdout_folder, saved_stdout_glob,
|
128
|
+
def list_recent_output(saved_stdout_folder, saved_stdout_glob,
|
129
|
+
list_count)
|
117
130
|
SavedFilesMatcher.most_recent_list(saved_stdout_folder,
|
118
131
|
saved_stdout_glob, list_count)
|
119
132
|
end
|
120
133
|
|
121
134
|
# :reek:UtilityFunction
|
122
|
-
def list_recent_scripts(saved_script_folder, saved_script_glob,
|
135
|
+
def list_recent_scripts(saved_script_folder, saved_script_glob,
|
136
|
+
list_count)
|
123
137
|
SavedFilesMatcher.most_recent_list(saved_script_folder,
|
124
138
|
saved_script_glob, list_count)
|
125
139
|
end
|
@@ -174,10 +188,10 @@ module MarkdownExec
|
|
174
188
|
FNR12 = ',~'
|
175
189
|
|
176
190
|
SHELL_COLOR_OPTIONS = {
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
191
|
+
BlockType::BASH => :menu_bash_color,
|
192
|
+
BlockType::LINK => :menu_link_color,
|
193
|
+
BlockType::OPTS => :menu_opts_color,
|
194
|
+
BlockType::VARS => :menu_vars_color
|
181
195
|
}.freeze
|
182
196
|
|
183
197
|
##
|
@@ -209,6 +223,22 @@ module MarkdownExec
|
|
209
223
|
@prompt = tty_prompt_without_disabled_symbol
|
210
224
|
end
|
211
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
|
+
|
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)
|
240
|
+
end
|
241
|
+
|
212
242
|
##
|
213
243
|
# Appends a summary of a block (FCB) to the blocks array.
|
214
244
|
#
|
@@ -218,38 +248,58 @@ module MarkdownExec
|
|
218
248
|
blocks.push get_block_summary(opts, fcb)
|
219
249
|
end
|
220
250
|
|
221
|
-
|
222
|
-
# Appends a final divider to the blocks array if it is specified in options.
|
251
|
+
# Appends a chrome block, which is a menu option for Back or Exit
|
223
252
|
#
|
224
|
-
|
225
|
-
|
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
|
226
267
|
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
oname: opts[:menu_final_divider] }
|
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
|
234
274
|
)
|
275
|
+
|
276
|
+
if insert_at_top
|
277
|
+
blocks_in_file.unshift(chrome_block)
|
278
|
+
else
|
279
|
+
blocks_in_file.push(chrome_block)
|
280
|
+
end
|
235
281
|
end
|
236
282
|
|
237
|
-
|
238
|
-
#
|
239
|
-
#
|
240
|
-
|
241
|
-
|
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
|
242
292
|
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
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
|
300
|
+
)
|
301
|
+
|
302
|
+
position == :initial ? blocks.unshift(divider) : blocks.push(divider)
|
253
303
|
end
|
254
304
|
|
255
305
|
# Execute a code block after approval and provide user interaction options.
|
@@ -261,14 +311,12 @@ module MarkdownExec
|
|
261
311
|
# @param opts [Hash] Options hash containing configuration settings.
|
262
312
|
# @param mdoc [YourMDocClass] An instance of the MDoc class.
|
263
313
|
#
|
264
|
-
def approve_and_execute_block(opts, mdoc)
|
265
|
-
selected
|
266
|
-
|
267
|
-
if selected.fetch(:shell, '') == BLOCK_TYPE_LINK
|
314
|
+
def approve_and_execute_block(selected, opts, mdoc)
|
315
|
+
if selected.fetch(:shell, '') == BlockType::LINK
|
268
316
|
handle_shell_link(opts, selected.fetch(:body, ''), mdoc)
|
269
|
-
elsif opts.fetch(:
|
317
|
+
elsif opts.fetch(:s_back, false)
|
270
318
|
handle_back_link(opts)
|
271
|
-
elsif selected[:shell] ==
|
319
|
+
elsif selected[:shell] == BlockType::OPTS
|
272
320
|
handle_shell_opts(opts, selected)
|
273
321
|
else
|
274
322
|
handle_remainder_blocks(mdoc, opts, selected)
|
@@ -302,10 +350,23 @@ module MarkdownExec
|
|
302
350
|
env_str(item[:env_var],
|
303
351
|
default: OptionValue.for_hash(item_default))
|
304
352
|
end
|
305
|
-
[item[:opt_name],
|
353
|
+
[item[:opt_name],
|
354
|
+
item[:proccode] ? item[:proccode].call(value) : value]
|
306
355
|
end.compact.to_h
|
307
356
|
end
|
308
357
|
|
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
|
+
|
309
370
|
def blocks_per_opts(blocks, opts)
|
310
371
|
return blocks if opts[:struct]
|
311
372
|
|
@@ -347,7 +408,7 @@ module MarkdownExec
|
|
347
408
|
# @return [Array<String>] Required code blocks as an array of lines.
|
348
409
|
def collect_required_code_lines(mdoc, selected, opts: {})
|
349
410
|
# Apply hash in opts block to environment variables
|
350
|
-
if selected[:shell] ==
|
411
|
+
if selected[:shell] == BlockType::VARS
|
351
412
|
data = YAML.load(selected[:body].join("\n"))
|
352
413
|
data.each_key do |key|
|
353
414
|
ENV[key] = value = data[key].to_s
|
@@ -361,7 +422,8 @@ module MarkdownExec
|
|
361
422
|
end
|
362
423
|
end
|
363
424
|
|
364
|
-
required = mdoc.collect_recursively_required_code(opts[:block_name],
|
425
|
+
required = mdoc.collect_recursively_required_code(opts[:block_name],
|
426
|
+
opts: opts)
|
365
427
|
read_required_blocks_from_temp_file + required[:code]
|
366
428
|
end
|
367
429
|
|
@@ -417,6 +479,20 @@ module MarkdownExec
|
|
417
479
|
fout "Error ENOENT: #{err.inspect}"
|
418
480
|
end
|
419
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
|
+
|
420
496
|
def copy_to_clipboard(required_lines)
|
421
497
|
text = required_lines.flatten.join($INPUT_RECORD_SEPARATOR)
|
422
498
|
Clipboard.copy(text)
|
@@ -434,7 +510,57 @@ module MarkdownExec
|
|
434
510
|
cnt / 2
|
435
511
|
end
|
436
512
|
|
437
|
-
|
513
|
+
##
|
514
|
+
# Creates and adds a formatted block to the blocks array based on the provided match and format options.
|
515
|
+
# @param blocks [Array] The array of blocks to add the new block to.
|
516
|
+
# @param fcb [FCB] The file control block containing the line to match against.
|
517
|
+
# @param match_data [MatchData] The match data containing named captures for formatting.
|
518
|
+
# @param format_option [String] The format string to be used for the new block.
|
519
|
+
# @param color_method [Symbol] The color method to apply to the block's display name.
|
520
|
+
def create_and_add_chrome_block(blocks, _fcb, match_data, format_option,
|
521
|
+
color_method)
|
522
|
+
oname = format(format_option,
|
523
|
+
match_data.named_captures.transform_keys(&:to_sym))
|
524
|
+
blocks.push FCB.new(
|
525
|
+
chrome: true,
|
526
|
+
disabled: '',
|
527
|
+
dname: oname.send(color_method),
|
528
|
+
oname: oname
|
529
|
+
)
|
530
|
+
end
|
531
|
+
|
532
|
+
##
|
533
|
+
# Processes lines within the file and converts them into blocks if they match certain criteria.
|
534
|
+
# @param blocks [Array] The array to append new blocks to.
|
535
|
+
# @param fcb [FCB] The file control block being processed.
|
536
|
+
# @param opts [Hash] Options containing configuration for line processing.
|
537
|
+
# @param use_chrome [Boolean] Indicates if the chrome styling should be applied.
|
538
|
+
def create_and_add_chrome_blocks(blocks, fcb, opts, use_chrome)
|
539
|
+
return unless use_chrome
|
540
|
+
|
541
|
+
match_criteria = [
|
542
|
+
{ match: :menu_task_match, format: :menu_task_format,
|
543
|
+
color: :menu_task_color },
|
544
|
+
{ match: :menu_divider_match, format: :menu_divider_format,
|
545
|
+
color: :menu_divider_color },
|
546
|
+
{ match: :menu_note_match, format: :menu_note_format,
|
547
|
+
color: :menu_note_color }
|
548
|
+
]
|
549
|
+
|
550
|
+
match_criteria.each do |criteria|
|
551
|
+
unless opts[criteria[:match]].present? &&
|
552
|
+
(mbody = fcb.body[0].match opts[criteria[:match]])
|
553
|
+
next
|
554
|
+
end
|
555
|
+
|
556
|
+
create_and_add_chrome_block(blocks, fcb, mbody, opts[criteria[:format]],
|
557
|
+
opts[criteria[:color]].to_sym)
|
558
|
+
break
|
559
|
+
end
|
560
|
+
end
|
561
|
+
|
562
|
+
def create_and_write_file_with_permissions(file_path, content,
|
563
|
+
chmod_value)
|
438
564
|
dirname = File.dirname(file_path)
|
439
565
|
FileUtils.mkdir_p dirname
|
440
566
|
File.write(file_path, content)
|
@@ -450,13 +576,29 @@ module MarkdownExec
|
|
450
576
|
def delete_required_temp_file
|
451
577
|
temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
|
452
578
|
|
453
|
-
|
579
|
+
if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
|
580
|
+
return
|
581
|
+
end
|
454
582
|
|
455
583
|
FileUtils.rm_f(temp_blocks_file_path)
|
456
584
|
|
457
585
|
clear_required_file
|
458
586
|
end
|
459
587
|
|
588
|
+
# Derives a title from the body of an FCB object.
|
589
|
+
# @param fcb [Object] The FCB object whose title is to be derived.
|
590
|
+
# @return [String] The derived title.
|
591
|
+
def derive_title_from_body(fcb)
|
592
|
+
body_content = fcb&.body
|
593
|
+
return '' unless body_content
|
594
|
+
|
595
|
+
if body_content.count == 1
|
596
|
+
body_content.first
|
597
|
+
else
|
598
|
+
format_multiline_body_as_title(body_content)
|
599
|
+
end
|
600
|
+
end
|
601
|
+
|
460
602
|
## Determines the correct filename to use for searching files
|
461
603
|
#
|
462
604
|
def determine_filename(specified_filename: nil, specified_folder: nil, default_filename: nil,
|
@@ -464,7 +606,8 @@ module MarkdownExec
|
|
464
606
|
if specified_filename&.present?
|
465
607
|
return specified_filename if specified_filename.start_with?('/')
|
466
608
|
|
467
|
-
File.join(specified_folder || default_folder,
|
609
|
+
File.join(specified_folder || default_folder,
|
610
|
+
specified_filename)
|
468
611
|
elsif specified_folder&.present?
|
469
612
|
File.join(specified_folder,
|
470
613
|
filetree ? @options[:md_filename_match] : @options[:md_filename_glob])
|
@@ -486,7 +629,7 @@ module MarkdownExec
|
|
486
629
|
command_execute(
|
487
630
|
opts,
|
488
631
|
required_lines.flatten.join("\n"),
|
489
|
-
args: opts.fetch(:
|
632
|
+
args: opts.fetch(:s_pass_args, [])
|
490
633
|
)
|
491
634
|
initialize_and_save_execution_output
|
492
635
|
output_execution_summary
|
@@ -496,20 +639,22 @@ module MarkdownExec
|
|
496
639
|
# Reports and executes block logic
|
497
640
|
def execute_block_logic(files)
|
498
641
|
@options[:filename] = select_document_if_multiple(files)
|
499
|
-
select_approve_and_execute_block({
|
500
|
-
|
501
|
-
struct: true
|
502
|
-
})
|
642
|
+
select_approve_and_execute_block({ bash: true,
|
643
|
+
struct: true })
|
503
644
|
end
|
504
645
|
|
505
646
|
## Executes the block specified in the options
|
506
647
|
#
|
507
648
|
def execute_block_with_error_handling(rest)
|
508
649
|
finalize_cli_argument_processing(rest)
|
509
|
-
@options[:
|
650
|
+
@options[:s_cli_rest] = rest
|
510
651
|
execute_code_block_based_on_options(@options)
|
511
652
|
rescue FileMissingError => err
|
512
653
|
puts "File missing: #{err}"
|
654
|
+
rescue StandardError => err
|
655
|
+
warn(error = "ERROR ** MarkParse.execute_block_with_error_handling(); #{err.inspect}")
|
656
|
+
binding.pry if $tap_enable
|
657
|
+
raise ArgumentError, error
|
513
658
|
end
|
514
659
|
|
515
660
|
# Main method to execute a block based on options and block_name
|
@@ -521,7 +666,8 @@ module MarkdownExec
|
|
521
666
|
doc_glob: -> { fout options[:md_filename_glob] },
|
522
667
|
list_blocks: lambda do
|
523
668
|
fout_list (files.map do |file|
|
524
|
-
|
669
|
+
menu_with_block_labels(filename: file,
|
670
|
+
struct: true)
|
525
671
|
end).flatten(1)
|
526
672
|
end,
|
527
673
|
list_default_yaml: -> { fout_list list_default_yaml },
|
@@ -571,15 +717,6 @@ module MarkdownExec
|
|
571
717
|
false
|
572
718
|
end
|
573
719
|
|
574
|
-
##
|
575
|
-
# Determines the types of blocks to select based on the filter.
|
576
|
-
#
|
577
|
-
def filter_block_types
|
578
|
-
## return type of blocks to select
|
579
|
-
#
|
580
|
-
%i[blocks line]
|
581
|
-
end
|
582
|
-
|
583
720
|
## post-parse options configuration
|
584
721
|
#
|
585
722
|
def finalize_cli_argument_processing(rest)
|
@@ -601,6 +738,16 @@ module MarkdownExec
|
|
601
738
|
@options[:block_name] = block_name if block_name.present?
|
602
739
|
end
|
603
740
|
|
741
|
+
# Formats multiline body content as a title string.
|
742
|
+
# indents all but first line with two spaces so it displays correctly in menu
|
743
|
+
# @param body_lines [Array<String>] The lines of body content.
|
744
|
+
# @return [String] Formatted title.
|
745
|
+
def format_multiline_body_as_title(body_lines)
|
746
|
+
body_lines.map.with_index do |line, index|
|
747
|
+
index.zero? ? line : " #{line}"
|
748
|
+
end.join("\n") << "\n"
|
749
|
+
end
|
750
|
+
|
604
751
|
## summarize blocks
|
605
752
|
#
|
606
753
|
def get_block_summary(call_options, fcb)
|
@@ -614,9 +761,12 @@ module MarkdownExec
|
|
614
761
|
else
|
615
762
|
fcb.title
|
616
763
|
end
|
617
|
-
bm = extract_named_captures_from_option(titlexcall,
|
618
|
-
|
619
|
-
fcb.
|
764
|
+
bm = extract_named_captures_from_option(titlexcall,
|
765
|
+
opts[:block_name_match])
|
766
|
+
fcb.stdin = extract_named_captures_from_option(titlexcall,
|
767
|
+
opts[:block_stdin_scan])
|
768
|
+
fcb.stdout = extract_named_captures_from_option(titlexcall,
|
769
|
+
opts[:block_stdout_scan])
|
620
770
|
|
621
771
|
shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
|
622
772
|
fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall
|
@@ -628,22 +778,13 @@ module MarkdownExec
|
|
628
778
|
fcb
|
629
779
|
end
|
630
780
|
|
631
|
-
##
|
632
|
-
# Handles errors that occur during the block listing process.
|
633
|
-
#
|
634
|
-
def handle_error(err)
|
635
|
-
warn(error = "ERROR ** MarkParse.list_blocks_in_file(); #{err.inspect}")
|
636
|
-
warn(caller[0..4])
|
637
|
-
raise StandardError, error
|
638
|
-
end
|
639
|
-
|
640
781
|
# Handles the link-back operation.
|
641
782
|
#
|
642
783
|
# @param opts [Hash] Configuration options hash.
|
643
|
-
# @return [Array<Symbol, String>] A tuple containing a
|
784
|
+
# @return [Array<Symbol, String>] A tuple containing a LoadFile flag and an empty string.
|
644
785
|
def handle_back_link(opts)
|
645
786
|
history_state_pop(opts)
|
646
|
-
[
|
787
|
+
[LoadFile::Load, '']
|
647
788
|
end
|
648
789
|
|
649
790
|
# Handles the execution and display of remainder blocks from a selected menu item.
|
@@ -651,18 +792,27 @@ module MarkdownExec
|
|
651
792
|
# @param mdoc [Object] Document object containing code blocks.
|
652
793
|
# @param opts [Hash] Configuration options hash.
|
653
794
|
# @param selected [Hash] Selected item from the menu.
|
654
|
-
# @return [Array<Symbol, String>] A tuple containing a
|
795
|
+
# @return [Array<Symbol, String>] A tuple containing a LoadFile flag and an empty string.
|
655
796
|
# @note The function can prompt the user for approval before executing code if opts[:user_must_approve] is true.
|
656
797
|
def handle_remainder_blocks(mdoc, opts, selected)
|
657
|
-
required_lines = collect_required_code_lines(mdoc, selected,
|
798
|
+
required_lines = collect_required_code_lines(mdoc, selected,
|
799
|
+
opts: opts)
|
658
800
|
if opts[:output_script] || opts[:user_must_approve]
|
659
801
|
display_required_code(opts, required_lines)
|
660
802
|
end
|
661
|
-
allow = opts[:user_must_approve]
|
662
|
-
|
663
|
-
|
803
|
+
allow = if opts[:user_must_approve]
|
804
|
+
prompt_for_user_approval(opts,
|
805
|
+
required_lines)
|
806
|
+
else
|
807
|
+
true
|
808
|
+
end
|
809
|
+
opts[:s_ir_approve] = allow
|
810
|
+
if opts[:s_ir_approve]
|
811
|
+
execute_approved_block(opts,
|
812
|
+
required_lines)
|
813
|
+
end
|
664
814
|
|
665
|
-
[
|
815
|
+
[LoadFile::Reuse, '']
|
666
816
|
end
|
667
817
|
|
668
818
|
# Handles the link-shell operation.
|
@@ -670,11 +820,11 @@ module MarkdownExec
|
|
670
820
|
# @param opts [Hash] Configuration options hash.
|
671
821
|
# @param body [Array<String>] The body content.
|
672
822
|
# @param mdoc [Object] Document object containing code blocks.
|
673
|
-
# @return [Array<Symbol, String>] A tuple containing a
|
823
|
+
# @return [Array<Symbol, String>] A tuple containing a LoadFile flag and a block name.
|
674
824
|
def handle_shell_link(opts, body, mdoc)
|
675
825
|
data = body.present? ? YAML.load(body.join("\n")) : {}
|
676
826
|
data_file = data.fetch('file', nil)
|
677
|
-
return [
|
827
|
+
return [LoadFile::Reuse, ''] unless data_file
|
678
828
|
|
679
829
|
history_state_push(mdoc, data_file, opts)
|
680
830
|
|
@@ -682,18 +832,19 @@ module MarkdownExec
|
|
682
832
|
ENV[var[0]] = var[1].to_s
|
683
833
|
end
|
684
834
|
|
685
|
-
[
|
835
|
+
[LoadFile::Load, data.fetch('block', '')]
|
686
836
|
end
|
687
837
|
|
688
838
|
# Handles options for the shell.
|
689
839
|
#
|
690
840
|
# @param opts [Hash] Configuration options hash.
|
691
841
|
# @param selected [Hash] Selected item from the menu.
|
692
|
-
# @return [Array<Symbol, String>] A tuple containing a
|
693
|
-
def handle_shell_opts(opts, selected)
|
842
|
+
# @return [Array<Symbol, String>] A tuple containing a LoadFile::Reuse flag and an empty string.
|
843
|
+
def handle_shell_opts(opts, selected, tgt2 = nil)
|
694
844
|
data = YAML.load(selected[:body].join("\n"))
|
695
845
|
data.each_key do |key|
|
696
|
-
opts[key.to_sym] = value = data[key]
|
846
|
+
opts[key.to_sym] = value = data[key]
|
847
|
+
tgt2[key.to_sym] = value if tgt2
|
697
848
|
next unless opts[:menu_opts_set_format].present?
|
698
849
|
|
699
850
|
print format(
|
@@ -702,7 +853,7 @@ module MarkdownExec
|
|
702
853
|
value: value }
|
703
854
|
).send(opts[:menu_opts_set_color].to_sym)
|
704
855
|
end
|
705
|
-
[
|
856
|
+
[LoadFile::Reuse, '']
|
706
857
|
end
|
707
858
|
|
708
859
|
# Handles reading and processing lines from a given IO stream
|
@@ -712,7 +863,8 @@ module MarkdownExec
|
|
712
863
|
def handle_stream(opts, stream, file_type, swap: false)
|
713
864
|
Thread.new do
|
714
865
|
until (line = stream.gets).nil?
|
715
|
-
@execute_files[file_type] =
|
866
|
+
@execute_files[file_type] =
|
867
|
+
@execute_files[file_type] + [line.strip]
|
716
868
|
print line if opts[:output_stdout]
|
717
869
|
yield line if block_given?
|
718
870
|
end
|
@@ -751,11 +903,22 @@ module MarkdownExec
|
|
751
903
|
ENV[MDE_HISTORY_ENV_NAME] = new_history
|
752
904
|
end
|
753
905
|
|
906
|
+
# Indents all lines in a given string with a specified indentation string.
|
907
|
+
# @param body [String] A multi-line string to be indented.
|
908
|
+
# @param indent [String] The string used for indentation (default is an empty string).
|
909
|
+
# @return [String] A single string with each line indented as specified.
|
910
|
+
def indent_all_lines(body, indent = nil)
|
911
|
+
return body if !indent.present?
|
912
|
+
|
913
|
+
body.lines.map { |line| indent + line.chomp }.join("\n")
|
914
|
+
end
|
915
|
+
|
754
916
|
## Sets up the options and returns the parsed arguments
|
755
917
|
#
|
756
918
|
def initialize_and_parse_cli_options
|
757
919
|
@options = base_options
|
758
|
-
read_configuration_file!(@options,
|
920
|
+
read_configuration_file!(@options,
|
921
|
+
".#{MarkdownExec::APP_NAME.downcase}.yml")
|
759
922
|
|
760
923
|
@option_parser = OptionParser.new do |opts|
|
761
924
|
executable_name = File.basename($PROGRAM_NAME)
|
@@ -773,7 +936,7 @@ module MarkdownExec
|
|
773
936
|
@option_parser.environment
|
774
937
|
|
775
938
|
rest = @option_parser.parse!(arguments_for_mde)
|
776
|
-
@options[:
|
939
|
+
@options[:s_pass_args] = ARGV[rest.count + 1..]
|
777
940
|
|
778
941
|
rest
|
779
942
|
end
|
@@ -783,7 +946,8 @@ module MarkdownExec
|
|
783
946
|
|
784
947
|
@options[:logged_stdout_filename] =
|
785
948
|
SavedAsset.stdout_name(blockname: @options[:block_name],
|
786
|
-
filename: File.basename(@options[:filename],
|
949
|
+
filename: File.basename(@options[:filename],
|
950
|
+
'.*'),
|
787
951
|
prefix: @options[:logged_stdout_filename_prefix],
|
788
952
|
time: Time.now.utc)
|
789
953
|
|
@@ -812,83 +976,56 @@ module MarkdownExec
|
|
812
976
|
|
813
977
|
state = initialize_state(opts)
|
814
978
|
|
815
|
-
# get type of messages to select
|
816
979
|
selected_messages = yield :filter
|
817
980
|
|
818
981
|
cfile.readlines(opts[:filename]).each do |line|
|
819
982
|
next unless line
|
820
983
|
|
821
|
-
update_line_and_block_state(line, state, opts, selected_messages,
|
984
|
+
update_line_and_block_state(line, state, opts, selected_messages,
|
985
|
+
&block)
|
822
986
|
end
|
823
987
|
end
|
824
988
|
|
825
989
|
##
|
826
|
-
# Returns a
|
827
|
-
# The
|
828
|
-
#
|
829
|
-
# @
|
830
|
-
|
831
|
-
|
832
|
-
|
833
|
-
|
834
|
-
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
858
|
-
|
859
|
-
|
860
|
-
|
861
|
-
|
862
|
-
|
863
|
-
if opts[:menu_divider_match].present? &&
|
864
|
-
(mbody = fcb.body[0].match opts[:menu_divider_match])
|
865
|
-
if use_chrome
|
866
|
-
blocks.push FCB.new(
|
867
|
-
{ chrome: true,
|
868
|
-
disabled: '',
|
869
|
-
dname: format(opts[:menu_divider_format],
|
870
|
-
mbody[:name]).send(opts[:menu_divider_color].to_sym),
|
871
|
-
oname: mbody[:name] }
|
872
|
-
)
|
873
|
-
end
|
874
|
-
elsif opts[:menu_task_match].present? &&
|
875
|
-
(fcb.body[0].match opts[:menu_task_match])
|
876
|
-
if use_chrome
|
877
|
-
blocks.push FCB.new(
|
878
|
-
{ chrome: true,
|
879
|
-
disabled: '',
|
880
|
-
dname: format(
|
881
|
-
opts[:menu_task_format],
|
882
|
-
$~.named_captures.transform_keys(&:to_sym)
|
883
|
-
).send(opts[:menu_task_color].to_sym),
|
884
|
-
oname: format(
|
885
|
-
opts[:menu_task_format],
|
886
|
-
$~.named_captures.transform_keys(&:to_sym)
|
887
|
-
) }
|
888
|
-
)
|
889
|
-
end
|
990
|
+
# Returns a lambda expression based on the given procname.
|
991
|
+
# @param procname [String] The name of the process to generate a lambda for.
|
992
|
+
# @param options [Hash] The options hash, necessary for some lambdas to access.
|
993
|
+
# @return [Lambda] The corresponding lambda expression.
|
994
|
+
def lambda_for_procname(procname, options)
|
995
|
+
case procname
|
996
|
+
when 'debug'
|
997
|
+
lambda { |value|
|
998
|
+
tap_config value: value
|
999
|
+
}
|
1000
|
+
when 'exit'
|
1001
|
+
->(_) { exit }
|
1002
|
+
when 'help'
|
1003
|
+
lambda { |_|
|
1004
|
+
fout menu_help
|
1005
|
+
exit
|
1006
|
+
}
|
1007
|
+
when 'path'
|
1008
|
+
->(value) { read_configuration_file!(options, value) }
|
1009
|
+
when 'show_config'
|
1010
|
+
lambda { |_|
|
1011
|
+
finalize_cli_argument_processing(options)
|
1012
|
+
fout options.sort_by_key.to_yaml
|
1013
|
+
}
|
1014
|
+
when 'val_as_bool'
|
1015
|
+
lambda { |value|
|
1016
|
+
value.instance_of?(::String) ? (value.chomp != '0') : value
|
1017
|
+
}
|
1018
|
+
when 'val_as_int'
|
1019
|
+
->(value) { value.to_i }
|
1020
|
+
when 'val_as_str'
|
1021
|
+
->(value) { value.to_s }
|
1022
|
+
when 'version'
|
1023
|
+
lambda { |_|
|
1024
|
+
fout MarkdownExec::VERSION
|
1025
|
+
exit
|
1026
|
+
}
|
890
1027
|
else
|
891
|
-
|
1028
|
+
procname
|
892
1029
|
end
|
893
1030
|
end
|
894
1031
|
|
@@ -944,38 +1081,68 @@ module MarkdownExec
|
|
944
1081
|
#
|
945
1082
|
def list_named_blocks_in_file(call_options = {}, &options_block)
|
946
1083
|
opts = optsmerge call_options, options_block
|
1084
|
+
blocks_per_opts(
|
1085
|
+
menu_from_file(opts.merge(struct: true)).select do |fcb|
|
1086
|
+
Filter.fcb_select?(opts.merge(no_chrome: true), fcb)
|
1087
|
+
end, opts
|
1088
|
+
)
|
1089
|
+
end
|
1090
|
+
|
1091
|
+
# return true if values were modified
|
1092
|
+
# execute block once per filename
|
1093
|
+
#
|
1094
|
+
def load_auto_blocks(opts, blocks_in_file)
|
1095
|
+
return unless opts[:document_load_opts_block_name].present?
|
1096
|
+
return if opts[:s_most_recent_filename] == opts[:filename]
|
947
1097
|
|
948
|
-
|
949
|
-
|
950
|
-
|
1098
|
+
block = block_find(blocks_in_file, :oname,
|
1099
|
+
opts[:document_load_opts_block_name])
|
1100
|
+
return unless block
|
1101
|
+
|
1102
|
+
handle_shell_opts(opts, block, @options)
|
1103
|
+
opts[:s_most_recent_filename] = opts[:filename]
|
1104
|
+
true
|
1105
|
+
end
|
1106
|
+
|
1107
|
+
def mdoc_and_menu_from_file(opts)
|
1108
|
+
menu_blocks = menu_from_file(opts.merge(struct: true))
|
1109
|
+
mdoc = MDoc.new(menu_blocks) do |nopts|
|
1110
|
+
opts.merge!(nopts)
|
951
1111
|
end
|
952
|
-
|
1112
|
+
[menu_blocks, mdoc]
|
953
1113
|
end
|
954
1114
|
|
955
1115
|
## Handles the file loading and returns the blocks in the file and MDoc instance
|
956
1116
|
#
|
957
|
-
def
|
958
|
-
blocks_in_file =
|
959
|
-
|
960
|
-
|
1117
|
+
def mdoc_menu_and_selected_from_file(opts)
|
1118
|
+
blocks_in_file, mdoc = mdoc_and_menu_from_file(opts)
|
1119
|
+
if load_auto_blocks(opts, blocks_in_file)
|
1120
|
+
# recreate menu with new options
|
1121
|
+
#
|
1122
|
+
blocks_in_file, mdoc = mdoc_and_menu_from_file(opts)
|
961
1123
|
end
|
1124
|
+
|
962
1125
|
blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
|
1126
|
+
add_menu_chrome_blocks!(blocks_menu)
|
963
1127
|
[blocks_in_file, blocks_menu, mdoc]
|
964
1128
|
end
|
965
1129
|
|
966
|
-
def
|
967
|
-
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
|
972
|
-
|
973
|
-
|
974
|
-
|
975
|
-
|
976
|
-
|
977
|
-
|
978
|
-
|
1130
|
+
def menu_chrome_colored_option(opts,
|
1131
|
+
option_symbol = :menu_option_back_name)
|
1132
|
+
if opts[:menu_chrome_color]
|
1133
|
+
menu_chrome_formatted_option(opts,
|
1134
|
+
option_symbol).send(opts[:menu_chrome_color].to_sym)
|
1135
|
+
else
|
1136
|
+
menu_chrome_formatted_option(opts, option_symbol)
|
1137
|
+
end
|
1138
|
+
end
|
1139
|
+
|
1140
|
+
def menu_chrome_formatted_option(opts,
|
1141
|
+
option_symbol = :menu_option_back_name)
|
1142
|
+
val1 = safeval(opts.fetch(option_symbol, ''))
|
1143
|
+
val1 unless opts[:menu_chrome_format]
|
1144
|
+
|
1145
|
+
format(opts[:menu_chrome_format], val1)
|
979
1146
|
end
|
980
1147
|
|
981
1148
|
def menu_export(data = menu_for_optparse)
|
@@ -985,76 +1152,49 @@ module MarkdownExec
|
|
985
1152
|
end.to_yaml
|
986
1153
|
end
|
987
1154
|
|
988
|
-
|
989
|
-
|
990
|
-
|
991
|
-
iter_blocks_in_file(options) do |btype, fcb|
|
992
|
-
case btype
|
993
|
-
when :filter
|
994
|
-
%i[blocks line]
|
995
|
-
when :line
|
996
|
-
if options[:menu_divider_match] &&
|
997
|
-
(mbody = fcb.body[0].match(options[:menu_divider_match]))
|
998
|
-
menu.push FCB.new({ dname: mbody[:name], oname: mbody[:name], disabled: '' })
|
999
|
-
end
|
1000
|
-
when :blocks
|
1001
|
-
menu += [fcb.oname]
|
1002
|
-
end
|
1003
|
-
end
|
1004
|
-
menu
|
1005
|
-
end
|
1006
|
-
|
1007
|
-
# :reek:DuplicateMethodCall
|
1008
|
-
# :reek:NestedIterators
|
1155
|
+
##
|
1156
|
+
# Generates a menu suitable for OptionParser from the menu items defined in YAML format.
|
1157
|
+
# @return [Array<Hash>] The array of option hashes for OptionParser.
|
1009
1158
|
def menu_for_optparse
|
1010
1159
|
menu_from_yaml.map do |menu_item|
|
1011
1160
|
menu_item.merge(
|
1012
|
-
|
1013
|
-
|
1014
|
-
proccode: case menu_item[:procname]
|
1015
|
-
when 'debug'
|
1016
|
-
lambda { |value|
|
1017
|
-
tap_config value: value
|
1018
|
-
}
|
1019
|
-
when 'exit'
|
1020
|
-
lambda { |_|
|
1021
|
-
exit
|
1022
|
-
}
|
1023
|
-
when 'help'
|
1024
|
-
lambda { |_|
|
1025
|
-
fout menu_help
|
1026
|
-
exit
|
1027
|
-
}
|
1028
|
-
when 'path'
|
1029
|
-
lambda { |value|
|
1030
|
-
read_configuration_file!(options, value)
|
1031
|
-
}
|
1032
|
-
when 'show_config'
|
1033
|
-
lambda { |_|
|
1034
|
-
finalize_cli_argument_processing(options)
|
1035
|
-
fout options.sort_by_key.to_yaml
|
1036
|
-
}
|
1037
|
-
when 'val_as_bool'
|
1038
|
-
lambda { |value|
|
1039
|
-
value.instance_of?(::String) ? (value.chomp != '0') : value
|
1040
|
-
}
|
1041
|
-
when 'val_as_int'
|
1042
|
-
->(value) { value.to_i }
|
1043
|
-
when 'val_as_str'
|
1044
|
-
->(value) { value.to_s }
|
1045
|
-
when 'version'
|
1046
|
-
lambda { |_|
|
1047
|
-
fout MarkdownExec::VERSION
|
1048
|
-
exit
|
1049
|
-
}
|
1050
|
-
else
|
1051
|
-
menu_item[:procname]
|
1052
|
-
end
|
1053
|
-
}
|
1161
|
+
opt_name: menu_item[:opt_name]&.to_sym,
|
1162
|
+
proccode: lambda_for_procname(menu_item[:procname], options)
|
1054
1163
|
)
|
1055
1164
|
end
|
1056
1165
|
end
|
1057
1166
|
|
1167
|
+
##
|
1168
|
+
# Returns a list of blocks in a given file, including dividers, tasks, and other types of blocks.
|
1169
|
+
# The list can be customized via call_options and options_block.
|
1170
|
+
#
|
1171
|
+
# @param call_options [Hash] Options passed as an argument.
|
1172
|
+
# @param options_block [Proc] Block for dynamic option manipulation.
|
1173
|
+
# @return [Array<FCB>] An array of FCB objects representing the blocks.
|
1174
|
+
#
|
1175
|
+
def menu_from_file(call_options = {},
|
1176
|
+
&options_block)
|
1177
|
+
opts = optsmerge(call_options, options_block)
|
1178
|
+
use_chrome = !opts[:no_chrome]
|
1179
|
+
|
1180
|
+
blocks = []
|
1181
|
+
iter_blocks_in_file(opts) do |btype, fcb|
|
1182
|
+
case btype
|
1183
|
+
when :blocks
|
1184
|
+
append_block_summary(blocks, fcb, opts)
|
1185
|
+
when :filter # what btypes are responded to?
|
1186
|
+
%i[blocks line]
|
1187
|
+
when :line
|
1188
|
+
create_and_add_chrome_blocks(blocks, fcb, opts, use_chrome)
|
1189
|
+
end
|
1190
|
+
end
|
1191
|
+
blocks
|
1192
|
+
rescue StandardError => err
|
1193
|
+
warn(error = "ERROR ** MarkParse.menu_from_file(); #{err.inspect}")
|
1194
|
+
warn(caller[0..4])
|
1195
|
+
raise StandardError, error
|
1196
|
+
end
|
1197
|
+
|
1058
1198
|
def menu_help
|
1059
1199
|
@option_parser.help
|
1060
1200
|
end
|
@@ -1064,7 +1204,9 @@ module MarkdownExec
|
|
1064
1204
|
end
|
1065
1205
|
|
1066
1206
|
def menu_option_append(opts, options, item)
|
1067
|
-
|
1207
|
+
unless item[:long_name].present? || item[:short_name].present?
|
1208
|
+
return
|
1209
|
+
end
|
1068
1210
|
|
1069
1211
|
opts.on(*[
|
1070
1212
|
# - long name
|
@@ -1077,7 +1219,9 @@ module MarkdownExec
|
|
1077
1219
|
|
1078
1220
|
# - description and default
|
1079
1221
|
[item[:description],
|
1080
|
-
(
|
1222
|
+
(if item[:default].present?
|
1223
|
+
"[#{value_for_cli item[:default]}]"
|
1224
|
+
end)].compact.join(' '),
|
1081
1225
|
|
1082
1226
|
# apply proccode, if present, to value
|
1083
1227
|
# save value to options hash if option is named
|
@@ -1090,9 +1234,24 @@ module MarkdownExec
|
|
1090
1234
|
].compact)
|
1091
1235
|
end
|
1092
1236
|
|
1237
|
+
def menu_with_block_labels(call_options = {})
|
1238
|
+
opts = options.merge(call_options)
|
1239
|
+
menu_from_file(opts).map do |fcb|
|
1240
|
+
BlockLabel.make(
|
1241
|
+
filename: opts[:filename],
|
1242
|
+
headings: fcb.fetch(:headings, []),
|
1243
|
+
menu_blocks_with_docname: opts[:menu_blocks_with_docname],
|
1244
|
+
menu_blocks_with_headings: opts[:menu_blocks_with_headings],
|
1245
|
+
title: fcb[:title],
|
1246
|
+
text: fcb[:text],
|
1247
|
+
body: fcb[:body]
|
1248
|
+
)
|
1249
|
+
end.compact
|
1250
|
+
end
|
1251
|
+
|
1093
1252
|
def next_block_name_from_command_line_arguments(opts)
|
1094
|
-
if opts[:
|
1095
|
-
opts[:block_name] = opts[:
|
1253
|
+
if opts[:s_cli_rest].present?
|
1254
|
+
opts[:block_name] = opts[:s_cli_rest].pop
|
1096
1255
|
false # repeat_menu
|
1097
1256
|
else
|
1098
1257
|
true # repeat_menu
|
@@ -1119,7 +1278,10 @@ module MarkdownExec
|
|
1119
1278
|
|
1120
1279
|
[['Script', :saved_filespec],
|
1121
1280
|
['StdOut', :logged_stdout_filespec]].each do |label, name|
|
1122
|
-
|
1281
|
+
if @options[name]
|
1282
|
+
oq << [label, @options[name],
|
1283
|
+
DISPLAY_LEVEL_ADMIN]
|
1284
|
+
end
|
1123
1285
|
end
|
1124
1286
|
|
1125
1287
|
oq.map do |label, value, level|
|
@@ -1150,9 +1312,11 @@ module MarkdownExec
|
|
1150
1312
|
def prepare_blocks_menu(blocks_in_file, opts)
|
1151
1313
|
# next if fcb.fetch(:disabled, false)
|
1152
1314
|
# next unless fcb.fetch(:name, '').present?
|
1153
|
-
blocks_in_file.map do |fcb|
|
1315
|
+
replace_consecutive_blanks(blocks_in_file).map do |fcb|
|
1316
|
+
next if Filter.prepared_not_in_menu?(opts, fcb)
|
1317
|
+
|
1154
1318
|
fcb.merge!(
|
1155
|
-
name: fcb.dname,
|
1319
|
+
name: indent_all_lines(fcb.dname, fcb.fetch(:indent, nil)),
|
1156
1320
|
label: BlockLabel.make(
|
1157
1321
|
body: fcb[:body],
|
1158
1322
|
filename: opts[:filename],
|
@@ -1176,7 +1340,7 @@ module MarkdownExec
|
|
1176
1340
|
fcb.oname = fcb.dname = fcb.title || ''
|
1177
1341
|
return unless fcb.body
|
1178
1342
|
|
1179
|
-
|
1343
|
+
update_title_from_body(fcb)
|
1180
1344
|
|
1181
1345
|
if block &&
|
1182
1346
|
selected_messages.include?(:blocks) &&
|
@@ -1194,6 +1358,13 @@ module MarkdownExec
|
|
1194
1358
|
block.call(:line, fcb)
|
1195
1359
|
end
|
1196
1360
|
|
1361
|
+
class MenuOptions
|
1362
|
+
YES = 1
|
1363
|
+
NO = 2
|
1364
|
+
SCRIPT_TO_CLIPBOARD = 3
|
1365
|
+
SAVE_SCRIPT = 4
|
1366
|
+
end
|
1367
|
+
|
1197
1368
|
##
|
1198
1369
|
# Presents a menu to the user for approving an action and performs additional tasks based on the selection.
|
1199
1370
|
# The function provides options for approval, rejection, copying data to clipboard, or saving data to a file.
|
@@ -1211,44 +1382,40 @@ module MarkdownExec
|
|
1211
1382
|
##
|
1212
1383
|
def prompt_for_user_approval(opts, required_lines)
|
1213
1384
|
# Present a selection menu for user approval.
|
1214
|
-
|
1215
|
-
|
1216
|
-
|
1217
|
-
menu.
|
1218
|
-
menu.choice opts[:
|
1219
|
-
menu.choice opts[:
|
1385
|
+
|
1386
|
+
sel = @prompt.select(opts[:prompt_approve_block],
|
1387
|
+
filter: true) do |menu|
|
1388
|
+
menu.default MenuOptions::YES
|
1389
|
+
menu.choice opts[:prompt_yes], MenuOptions::YES
|
1390
|
+
menu.choice opts[:prompt_no], MenuOptions::NO
|
1391
|
+
menu.choice opts[:prompt_script_to_clipboard],
|
1392
|
+
MenuOptions::SCRIPT_TO_CLIPBOARD
|
1393
|
+
menu.choice opts[:prompt_save_script], MenuOptions::SAVE_SCRIPT
|
1220
1394
|
end
|
1221
1395
|
|
1222
|
-
if sel ==
|
1396
|
+
if sel == MenuOptions::SCRIPT_TO_CLIPBOARD
|
1223
1397
|
copy_to_clipboard(required_lines)
|
1224
|
-
elsif sel ==
|
1398
|
+
elsif sel == MenuOptions::SAVE_SCRIPT
|
1225
1399
|
save_to_file(opts, required_lines)
|
1226
1400
|
end
|
1227
1401
|
|
1228
|
-
sel ==
|
1402
|
+
sel == MenuOptions::YES
|
1403
|
+
rescue TTY::Reader::InputInterrupt
|
1404
|
+
exit 1
|
1229
1405
|
end
|
1230
1406
|
|
1231
|
-
|
1232
|
-
|
1233
|
-
|
1234
|
-
|
1235
|
-
|
1236
|
-
|
1237
|
-
|
1238
|
-
|
1239
|
-
@hs_curr = state[:unit]
|
1240
|
-
@hs_rest = state[:rest]
|
1241
|
-
@options[:menu_back_at_top] ? [label] + items : items + [label]
|
1242
|
-
end
|
1243
|
-
|
1244
|
-
## insert exit option at head or tail
|
1245
|
-
#
|
1246
|
-
def prompt_menu_add_exit(items, label)
|
1247
|
-
if @options[:menu_exit_at_top]
|
1248
|
-
(@options[:menu_with_exit] ? [label] : []) + items
|
1249
|
-
else
|
1250
|
-
items + (@options[:menu_with_exit] ? [label] : [])
|
1407
|
+
def prompt_select_continue(opts)
|
1408
|
+
sel = @prompt.select(
|
1409
|
+
opts[:prompt_after_bash_exec],
|
1410
|
+
filter: true,
|
1411
|
+
quiet: true
|
1412
|
+
) do |menu|
|
1413
|
+
menu.choice opts[:prompt_yes]
|
1414
|
+
menu.choice opts[:prompt_exit]
|
1251
1415
|
end
|
1416
|
+
sel == opts[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
|
1417
|
+
rescue TTY::Reader::InputInterrupt
|
1418
|
+
exit 1
|
1252
1419
|
end
|
1253
1420
|
|
1254
1421
|
# :reek:UtilityFunction ### temp
|
@@ -1267,7 +1434,9 @@ module MarkdownExec
|
|
1267
1434
|
temp_blocks = []
|
1268
1435
|
|
1269
1436
|
temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
|
1270
|
-
|
1437
|
+
if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
|
1438
|
+
return temp_blocks
|
1439
|
+
end
|
1271
1440
|
|
1272
1441
|
if File.exist?(temp_blocks_file_path)
|
1273
1442
|
temp_blocks = File.readlines(temp_blocks_file_path, chomp: true)
|
@@ -1276,6 +1445,24 @@ module MarkdownExec
|
|
1276
1445
|
temp_blocks
|
1277
1446
|
end
|
1278
1447
|
|
1448
|
+
# Replace duplicate blanks (where :oname is not present) with a single blank line.
|
1449
|
+
#
|
1450
|
+
# @param [Array<Hash>] lines Array of hashes to process.
|
1451
|
+
# @return [Array<Hash>] Cleaned array with consecutive blanks collapsed into one.
|
1452
|
+
def replace_consecutive_blanks(lines)
|
1453
|
+
lines.chunk_while do |i, j|
|
1454
|
+
i[:oname].to_s.empty? && j[:oname].to_s.empty?
|
1455
|
+
end.map do |chunk|
|
1456
|
+
if chunk.any? do |line|
|
1457
|
+
line[:oname].to_s.strip.empty?
|
1458
|
+
end
|
1459
|
+
chunk.first
|
1460
|
+
else
|
1461
|
+
chunk
|
1462
|
+
end
|
1463
|
+
end.flatten
|
1464
|
+
end
|
1465
|
+
|
1279
1466
|
def run
|
1280
1467
|
clear_required_file
|
1281
1468
|
execute_block_with_error_handling(initialize_and_parse_cli_options)
|
@@ -1293,7 +1480,15 @@ module MarkdownExec
|
|
1293
1480
|
|
1294
1481
|
saved_name_split filename
|
1295
1482
|
@options[:save_executed_script] = false
|
1296
|
-
select_approve_and_execute_block
|
1483
|
+
select_approve_and_execute_block
|
1484
|
+
end
|
1485
|
+
|
1486
|
+
def safeval(str)
|
1487
|
+
eval(str)
|
1488
|
+
rescue StandardError
|
1489
|
+
warn $!
|
1490
|
+
binding.pry if $tap_enable
|
1491
|
+
raise StandardError, $!
|
1297
1492
|
end
|
1298
1493
|
|
1299
1494
|
def save_to_file(opts, required_lines)
|
@@ -1320,40 +1515,48 @@ module MarkdownExec
|
|
1320
1515
|
# @param call_options [Hash] Initial options for the method.
|
1321
1516
|
# @param options_block [Block] Block of options to be merged with call_options.
|
1322
1517
|
# @return [Nil] Returns nil if no code block is selected or an error occurs.
|
1323
|
-
def select_approve_and_execute_block(call_options,
|
1324
|
-
|
1325
|
-
|
1326
|
-
|
1327
|
-
|
1518
|
+
def select_approve_and_execute_block(call_options = {},
|
1519
|
+
&options_block)
|
1520
|
+
base_opts = optsmerge(call_options, options_block)
|
1521
|
+
repeat_menu = true && !base_opts[:block_name].present?
|
1522
|
+
load_file = LoadFile::Reuse
|
1523
|
+
default = nil
|
1524
|
+
block = nil
|
1328
1525
|
|
1329
1526
|
loop do
|
1330
1527
|
loop do
|
1331
|
-
opts
|
1332
|
-
|
1333
|
-
|
1334
|
-
|
1335
|
-
|
1336
|
-
|
1337
|
-
|
1338
|
-
|
1339
|
-
|
1340
|
-
|
1341
|
-
|
1342
|
-
|
1343
|
-
|
1344
|
-
|
1345
|
-
|
1528
|
+
opts = base_opts.dup
|
1529
|
+
opts[:s_back] = false
|
1530
|
+
blocks_in_file, blocks_menu, mdoc = mdoc_menu_and_selected_from_file(opts)
|
1531
|
+
block, state = command_or_user_selected_block(blocks_in_file, blocks_menu,
|
1532
|
+
default, opts)
|
1533
|
+
return if state == MenuState::EXIT
|
1534
|
+
|
1535
|
+
load_file, next_block_name = approve_and_execute_block(block, opts,
|
1536
|
+
mdoc)
|
1537
|
+
default = load_file == LoadFile::Load ? nil : opts[:block_name]
|
1538
|
+
base_opts[:block_name] = opts[:block_name] = next_block_name
|
1539
|
+
base_opts[:filename] = opts[:filename]
|
1540
|
+
|
1541
|
+
# user prompt to exit if the menu will be displayed again
|
1542
|
+
#
|
1543
|
+
if repeat_menu &&
|
1544
|
+
block[:shell] == BlockType::BASH &&
|
1545
|
+
opts[:pause_after_bash_exec] &&
|
1546
|
+
prompt_select_continue(opts) == MenuState::EXIT
|
1547
|
+
return
|
1346
1548
|
end
|
1347
1549
|
|
1348
|
-
|
1349
|
-
|
1350
|
-
|
1351
|
-
|
1550
|
+
# exit current document/menu if loading next document or single block_name was specified
|
1551
|
+
#
|
1552
|
+
if state == MenuState::CONTINUE && load_file == LoadFile::Load
|
1553
|
+
break
|
1554
|
+
end
|
1352
1555
|
break unless repeat_menu
|
1353
1556
|
end
|
1354
|
-
break if load_file
|
1557
|
+
break if load_file == LoadFile::Reuse
|
1355
1558
|
|
1356
|
-
repeat_menu = next_block_name_from_command_line_arguments(
|
1559
|
+
repeat_menu = next_block_name_from_command_line_arguments(base_opts)
|
1357
1560
|
end
|
1358
1561
|
rescue StandardError => err
|
1359
1562
|
warn(error = "ERROR ** MarkParse.select_approve_and_execute_block(); #{err.inspect}")
|
@@ -1383,21 +1586,24 @@ module MarkdownExec
|
|
1383
1586
|
# Presents a TTY prompt to select an option or exit, returns metadata including option and selected
|
1384
1587
|
def select_option_with_metadata(prompt_text, items, opts = {})
|
1385
1588
|
selection = @prompt.select(prompt_text,
|
1386
|
-
|
1387
|
-
prompt_menu_add_back(
|
1388
|
-
items,
|
1389
|
-
opts[:menu_option_back_name]
|
1390
|
-
),
|
1391
|
-
opts[:menu_option_exit_name]
|
1392
|
-
),
|
1589
|
+
items,
|
1393
1590
|
opts.merge(filter: true))
|
1394
|
-
|
1395
|
-
|
1396
|
-
|
1397
|
-
|
1398
|
-
|
1399
|
-
|
1400
|
-
|
1591
|
+
|
1592
|
+
items.find { |item| item[:dname] == selection }
|
1593
|
+
.merge(
|
1594
|
+
if selection == menu_chrome_colored_option(opts,
|
1595
|
+
:menu_option_back_name)
|
1596
|
+
{ option: selection, curr: @hs_curr, rest: @hs_rest,
|
1597
|
+
shell: BlockType::LINK }
|
1598
|
+
elsif selection == menu_chrome_colored_option(opts,
|
1599
|
+
:menu_option_exit_name)
|
1600
|
+
{ option: selection }
|
1601
|
+
else
|
1602
|
+
{ selected: selection }
|
1603
|
+
end
|
1604
|
+
)
|
1605
|
+
rescue TTY::Reader::InputInterrupt
|
1606
|
+
exit 1
|
1401
1607
|
end
|
1402
1608
|
|
1403
1609
|
def select_recent_output
|
@@ -1429,27 +1635,20 @@ module MarkdownExec
|
|
1429
1635
|
|
1430
1636
|
saved_name_split(filename)
|
1431
1637
|
|
1432
|
-
select_approve_and_execute_block({
|
1433
|
-
bash: true,
|
1638
|
+
select_approve_and_execute_block({ bash: true,
|
1434
1639
|
save_executed_script: false,
|
1435
|
-
struct: true
|
1436
|
-
})
|
1437
|
-
end
|
1438
|
-
|
1439
|
-
# set the title of an FCB object based on its body if it is nil or empty
|
1440
|
-
def set_fcb_title(fcb)
|
1441
|
-
return unless fcb.title.nil? || fcb.title.empty?
|
1442
|
-
|
1443
|
-
fcb.title = (fcb&.body || []).join(' ').gsub(/ +/, ' ')[0..64]
|
1640
|
+
struct: true })
|
1444
1641
|
end
|
1445
1642
|
|
1446
|
-
def start_fenced_block(opts, line, headings,
|
1643
|
+
def start_fenced_block(opts, line, headings,
|
1644
|
+
fenced_start_extended_regex)
|
1447
1645
|
fcb_title_groups = line.match(fenced_start_extended_regex).named_captures.sym_keys
|
1448
1646
|
rest = fcb_title_groups.fetch(:rest, '')
|
1449
1647
|
|
1450
1648
|
fcb = FCB.new
|
1451
1649
|
fcb.headings = headings
|
1452
1650
|
fcb.oname = fcb.dname = fcb_title_groups.fetch(:name, '')
|
1651
|
+
fcb.indent = fcb_title_groups.fetch(:indent, '')
|
1453
1652
|
fcb.shell = fcb_title_groups.fetch(:shell, '')
|
1454
1653
|
fcb.title = fcb_title_groups.fetch(:name, '')
|
1455
1654
|
fcb.body = []
|
@@ -1476,7 +1675,11 @@ module MarkdownExec
|
|
1476
1675
|
end
|
1477
1676
|
|
1478
1677
|
def tty_prompt_without_disabled_symbol
|
1479
|
-
TTY::Prompt.new(interrupt:
|
1678
|
+
TTY::Prompt.new(interrupt: lambda {
|
1679
|
+
puts;
|
1680
|
+
raise TTY::Reader::InputInterrupt
|
1681
|
+
},
|
1682
|
+
symbols: { cross: ' ' })
|
1480
1683
|
end
|
1481
1684
|
|
1482
1685
|
##
|
@@ -1523,7 +1726,8 @@ module MarkdownExec
|
|
1523
1726
|
#
|
1524
1727
|
# @return [Void] The function modifies the `state` and `selected_messages` arguments in place.
|
1525
1728
|
##
|
1526
|
-
def update_line_and_block_state(line, state, opts, selected_messages,
|
1729
|
+
def update_line_and_block_state(line, state, opts, selected_messages,
|
1730
|
+
&block)
|
1527
1731
|
if opts[:menu_blocks_with_headings]
|
1528
1732
|
state[:headings] =
|
1529
1733
|
update_document_headings(line, state[:headings], opts)
|
@@ -1531,7 +1735,8 @@ module MarkdownExec
|
|
1531
1735
|
|
1532
1736
|
if line.match(state[:fenced_start_and_end_regex])
|
1533
1737
|
if state[:in_fenced_block]
|
1534
|
-
process_fenced_block(state[:fcb], opts, selected_messages,
|
1738
|
+
process_fenced_block(state[:fcb], opts, selected_messages,
|
1739
|
+
&block)
|
1535
1740
|
state[:in_fenced_block] = false
|
1536
1741
|
else
|
1537
1742
|
state[:fcb] =
|
@@ -1540,7 +1745,13 @@ module MarkdownExec
|
|
1540
1745
|
state[:in_fenced_block] = true
|
1541
1746
|
end
|
1542
1747
|
elsif state[:in_fenced_block] && state[:fcb].body
|
1543
|
-
|
1748
|
+
## add line to fenced code block
|
1749
|
+
# remove fcb indent if possible
|
1750
|
+
#
|
1751
|
+
state[:fcb].body += [
|
1752
|
+
line.chomp.sub(/^#{state[:fcb].indent}/, '')
|
1753
|
+
]
|
1754
|
+
|
1544
1755
|
else
|
1545
1756
|
process_line(line, opts, selected_messages, &block)
|
1546
1757
|
end
|
@@ -1557,26 +1768,59 @@ module MarkdownExec
|
|
1557
1768
|
@options
|
1558
1769
|
end
|
1559
1770
|
|
1560
|
-
|
1771
|
+
# Updates the title of an FCB object from its body content if the title is nil or empty.
|
1772
|
+
def update_title_from_body(fcb)
|
1773
|
+
return unless fcb.title.nil? || fcb.title.empty?
|
1774
|
+
|
1775
|
+
fcb.title = derive_title_from_body(fcb)
|
1776
|
+
end
|
1777
|
+
|
1778
|
+
def wait_for_user_selected_block(blocks_in_file, blocks_menu,
|
1779
|
+
default, opts)
|
1780
|
+
block, state = wait_for_user_selection(blocks_in_file, blocks_menu,
|
1781
|
+
default, opts)
|
1782
|
+
case state
|
1783
|
+
when MenuState::BACK
|
1784
|
+
opts[:block_name] = block[:dname]
|
1785
|
+
opts[:s_back] = true
|
1786
|
+
when MenuState::CONTINUE
|
1787
|
+
opts[:block_name] = block[:dname]
|
1788
|
+
end
|
1789
|
+
|
1790
|
+
[block, state]
|
1791
|
+
end
|
1792
|
+
|
1793
|
+
## Handles the menu interaction and returns selected block and option state
|
1561
1794
|
#
|
1562
|
-
def wait_for_user_selection(blocks_in_file, blocks_menu, default,
|
1795
|
+
def wait_for_user_selection(blocks_in_file, blocks_menu, default,
|
1796
|
+
opts)
|
1563
1797
|
pt = opts[:prompt_select_block].to_s
|
1564
1798
|
bm = prepare_blocks_menu(blocks_menu, opts)
|
1565
|
-
return [nil,
|
1799
|
+
return [nil, MenuState::EXIT] if bm.count.zero?
|
1566
1800
|
|
1567
|
-
|
1568
|
-
|
1801
|
+
o2 = if default
|
1802
|
+
opts.merge(default: default)
|
1803
|
+
else
|
1804
|
+
opts
|
1805
|
+
end
|
1806
|
+
|
1807
|
+
obj = select_option_with_metadata(pt, bm, o2.merge(
|
1569
1808
|
per_page: opts[:select_page_height]
|
1570
1809
|
))
|
1571
|
-
|
1572
|
-
|
1573
|
-
|
1574
|
-
|
1575
|
-
|
1810
|
+
|
1811
|
+
case obj.fetch(:oname, nil)
|
1812
|
+
when menu_chrome_formatted_option(opts, :menu_option_exit_name)
|
1813
|
+
[nil, MenuState::EXIT]
|
1814
|
+
when menu_chrome_formatted_option(opts, :menu_option_back_name)
|
1815
|
+
[obj, MenuState::BACK]
|
1576
1816
|
else
|
1577
|
-
|
1578
|
-
[label_block.oname, :continue]
|
1817
|
+
[obj, MenuState::CONTINUE]
|
1579
1818
|
end
|
1819
|
+
rescue StandardError => err
|
1820
|
+
warn(error = "ERROR ** MarkParse.wait_for_user_selection(); #{err.inspect}")
|
1821
|
+
warn caller.take(3)
|
1822
|
+
binding.pry if $tap_enable
|
1823
|
+
raise ArgumentError, error
|
1580
1824
|
end
|
1581
1825
|
|
1582
1826
|
# Handles the core logic for generating the command file's metadata and content.
|
@@ -1593,7 +1837,8 @@ module MarkdownExec
|
|
1593
1837
|
|
1594
1838
|
@execute_script_filespec =
|
1595
1839
|
@options[:saved_filespec] =
|
1596
|
-
File.join opts[:saved_script_folder],
|
1840
|
+
File.join opts[:saved_script_folder],
|
1841
|
+
opts[:saved_script_filename]
|
1597
1842
|
|
1598
1843
|
shebang = if @options[:shebang]&.present?
|
1599
1844
|
"#{@options[:shebang]} #{@options[:shell]}\n"
|
@@ -1656,7 +1901,7 @@ if $PROGRAM_NAME == __FILE__
|
|
1656
1901
|
|
1657
1902
|
def test_calling_execute_approved_block_calls_command_execute_with_argument_args_value
|
1658
1903
|
pigeon = 'E'
|
1659
|
-
obj = {
|
1904
|
+
obj = { s_pass_args: pigeon }
|
1660
1905
|
|
1661
1906
|
c = MarkdownExec::MarkParse.new
|
1662
1907
|
|
@@ -1676,15 +1921,17 @@ if $PROGRAM_NAME == __FILE__
|
|
1676
1921
|
end
|
1677
1922
|
|
1678
1923
|
def test_set_fcb_title
|
1679
|
-
# sample input and output data for testing
|
1924
|
+
# sample input and output data for testing update_title_from_body method
|
1680
1925
|
input_output_data = [
|
1681
1926
|
{
|
1682
1927
|
input: FCB.new(title: nil, body: ["puts 'Hello, world!'"]),
|
1683
1928
|
output: "puts 'Hello, world!'"
|
1684
1929
|
},
|
1685
1930
|
{
|
1686
|
-
input: FCB.new(title: '',
|
1687
|
-
|
1931
|
+
input: FCB.new(title: '',
|
1932
|
+
body: ['def add(x, y)',
|
1933
|
+
' x + y', 'end']),
|
1934
|
+
output: "def add(x, y)\n x + y\n end\n"
|
1688
1935
|
},
|
1689
1936
|
{
|
1690
1937
|
input: FCB.new(title: 'foo', body: %w[bar baz]),
|
@@ -1697,10 +1944,20 @@ if $PROGRAM_NAME == __FILE__
|
|
1697
1944
|
input_output_data.each do |data|
|
1698
1945
|
input = data[:input]
|
1699
1946
|
output = data[:output]
|
1700
|
-
@mark_parse.
|
1947
|
+
@mark_parse.update_title_from_body(input)
|
1701
1948
|
assert_equal output, input.title
|
1702
1949
|
end
|
1703
1950
|
end
|
1704
1951
|
end
|
1705
|
-
|
1706
|
-
|
1952
|
+
|
1953
|
+
def test_select_block
|
1954
|
+
blocks = [block1, block2]
|
1955
|
+
menu = [m1, m2]
|
1956
|
+
|
1957
|
+
block, state = obj.select_block(blocks, menu, nil, {})
|
1958
|
+
|
1959
|
+
assert_equal block1, block
|
1960
|
+
assert_equal MenuState::CONTINUE, state
|
1961
|
+
end
|
1962
|
+
end # module MarkdownExec
|
1963
|
+
end # if
|