markdown_exec 1.4 → 1.5
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/.pryrc +3 -1
- data/.rubocop.yml +3 -1
- data/CHANGELOG.md +29 -0
- data/Gemfile.lock +1 -1
- data/bin/bmde +103 -1
- data/bin/tab_completion.sh +2 -2
- data/examples/linked1.md +1 -0
- data/examples/linked2.md +0 -1
- data/examples/opts.md +16 -4
- 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 +607 -341
- data/lib/mdoc.rb +106 -32
- data/lib/menu.src.yml +173 -6
- data/lib/menu.yml +62 -7
- metadata +4 -4
data/lib/markdown_exec.rb
CHANGED
@@ -9,6 +9,7 @@ require 'fileutils'
|
|
9
9
|
require 'open3'
|
10
10
|
require 'optparse'
|
11
11
|
require 'shellwords'
|
12
|
+
require 'tmpdir'
|
12
13
|
require 'tty-prompt'
|
13
14
|
require 'yaml'
|
14
15
|
|
@@ -37,10 +38,6 @@ $stdout.sync = true
|
|
37
38
|
|
38
39
|
MDE_HISTORY_ENV_NAME = 'MDE_MENU_HISTORY'
|
39
40
|
|
40
|
-
# macros
|
41
|
-
#
|
42
|
-
LOAD_FILE = true
|
43
|
-
|
44
41
|
# custom error: file specified is missing
|
45
42
|
#
|
46
43
|
class FileMissingError < StandardError; end
|
@@ -62,6 +59,17 @@ class Hash
|
|
62
59
|
end
|
63
60
|
end
|
64
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
|
+
|
65
73
|
# integer value for comparison
|
66
74
|
#
|
67
75
|
def options_fetch_display_level(options)
|
@@ -109,16 +117,23 @@ def dp(str)
|
|
109
117
|
lout " => #{str}", level: DISPLAY_LEVEL_DEBUG
|
110
118
|
end
|
111
119
|
|
120
|
+
def rpry
|
121
|
+
require 'pry-nav'
|
122
|
+
require 'pry-stack_explorer'
|
123
|
+
end
|
124
|
+
|
112
125
|
public
|
113
126
|
|
114
127
|
# :reek:UtilityFunction
|
115
|
-
def list_recent_output(saved_stdout_folder, saved_stdout_glob,
|
128
|
+
def list_recent_output(saved_stdout_folder, saved_stdout_glob,
|
129
|
+
list_count)
|
116
130
|
SavedFilesMatcher.most_recent_list(saved_stdout_folder,
|
117
131
|
saved_stdout_glob, list_count)
|
118
132
|
end
|
119
133
|
|
120
134
|
# :reek:UtilityFunction
|
121
|
-
def list_recent_scripts(saved_script_folder, saved_script_glob,
|
135
|
+
def list_recent_scripts(saved_script_folder, saved_script_glob,
|
136
|
+
list_count)
|
122
137
|
SavedFilesMatcher.most_recent_list(saved_script_folder,
|
123
138
|
saved_script_glob, list_count)
|
124
139
|
end
|
@@ -173,10 +188,10 @@ module MarkdownExec
|
|
173
188
|
FNR12 = ',~'
|
174
189
|
|
175
190
|
SHELL_COLOR_OPTIONS = {
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
191
|
+
BlockType::BASH => :menu_bash_color,
|
192
|
+
BlockType::LINK => :menu_link_color,
|
193
|
+
BlockType::OPTS => :menu_opts_color,
|
194
|
+
BlockType::VARS => :menu_vars_color
|
180
195
|
}.freeze
|
181
196
|
|
182
197
|
##
|
@@ -208,6 +223,22 @@ module MarkdownExec
|
|
208
223
|
@prompt = tty_prompt_without_disabled_symbol
|
209
224
|
end
|
210
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
|
+
|
211
242
|
##
|
212
243
|
# Appends a summary of a block (FCB) to the blocks array.
|
213
244
|
#
|
@@ -217,38 +248,58 @@ module MarkdownExec
|
|
217
248
|
blocks.push get_block_summary(opts, fcb)
|
218
249
|
end
|
219
250
|
|
220
|
-
|
221
|
-
# 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
|
222
252
|
#
|
223
|
-
|
224
|
-
|
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
|
225
267
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
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
|
233
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
|
234
281
|
end
|
235
282
|
|
236
|
-
|
237
|
-
#
|
238
|
-
#
|
239
|
-
|
240
|
-
|
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
|
241
292
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
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)
|
252
303
|
end
|
253
304
|
|
254
305
|
# Execute a code block after approval and provide user interaction options.
|
@@ -260,14 +311,12 @@ module MarkdownExec
|
|
260
311
|
# @param opts [Hash] Options hash containing configuration settings.
|
261
312
|
# @param mdoc [YourMDocClass] An instance of the MDoc class.
|
262
313
|
#
|
263
|
-
def approve_and_execute_block(opts, mdoc)
|
264
|
-
selected
|
265
|
-
|
266
|
-
if selected.fetch(:shell, '') == BLOCK_TYPE_LINK
|
314
|
+
def approve_and_execute_block(selected, opts, mdoc)
|
315
|
+
if selected.fetch(:shell, '') == BlockType::LINK
|
267
316
|
handle_shell_link(opts, selected.fetch(:body, ''), mdoc)
|
268
|
-
elsif opts.fetch(:
|
317
|
+
elsif opts.fetch(:s_back, false)
|
269
318
|
handle_back_link(opts)
|
270
|
-
elsif selected[:shell] ==
|
319
|
+
elsif selected[:shell] == BlockType::OPTS
|
271
320
|
handle_shell_opts(opts, selected)
|
272
321
|
else
|
273
322
|
handle_remainder_blocks(mdoc, opts, selected)
|
@@ -301,10 +350,23 @@ module MarkdownExec
|
|
301
350
|
env_str(item[:env_var],
|
302
351
|
default: OptionValue.for_hash(item_default))
|
303
352
|
end
|
304
|
-
[item[:opt_name],
|
353
|
+
[item[:opt_name],
|
354
|
+
item[:proccode] ? item[:proccode].call(value) : value]
|
305
355
|
end.compact.to_h
|
306
356
|
end
|
307
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
|
+
|
308
370
|
def blocks_per_opts(blocks, opts)
|
309
371
|
return blocks if opts[:struct]
|
310
372
|
|
@@ -346,7 +408,7 @@ module MarkdownExec
|
|
346
408
|
# @return [Array<String>] Required code blocks as an array of lines.
|
347
409
|
def collect_required_code_lines(mdoc, selected, opts: {})
|
348
410
|
# Apply hash in opts block to environment variables
|
349
|
-
if selected[:shell] ==
|
411
|
+
if selected[:shell] == BlockType::VARS
|
350
412
|
data = YAML.load(selected[:body].join("\n"))
|
351
413
|
data.each_key do |key|
|
352
414
|
ENV[key] = value = data[key].to_s
|
@@ -360,7 +422,8 @@ module MarkdownExec
|
|
360
422
|
end
|
361
423
|
end
|
362
424
|
|
363
|
-
required = mdoc.collect_recursively_required_code(opts[:block_name],
|
425
|
+
required = mdoc.collect_recursively_required_code(opts[:block_name],
|
426
|
+
opts: opts)
|
364
427
|
read_required_blocks_from_temp_file + required[:code]
|
365
428
|
end
|
366
429
|
|
@@ -416,6 +479,20 @@ module MarkdownExec
|
|
416
479
|
fout "Error ENOENT: #{err.inspect}"
|
417
480
|
end
|
418
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
|
+
|
419
496
|
def copy_to_clipboard(required_lines)
|
420
497
|
text = required_lines.flatten.join($INPUT_RECORD_SEPARATOR)
|
421
498
|
Clipboard.copy(text)
|
@@ -433,7 +510,48 @@ module MarkdownExec
|
|
433
510
|
cnt / 2
|
434
511
|
end
|
435
512
|
|
436
|
-
|
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)
|
437
555
|
dirname = File.dirname(file_path)
|
438
556
|
FileUtils.mkdir_p dirname
|
439
557
|
File.write(file_path, content)
|
@@ -449,13 +567,29 @@ module MarkdownExec
|
|
449
567
|
def delete_required_temp_file
|
450
568
|
temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
|
451
569
|
|
452
|
-
|
570
|
+
if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
|
571
|
+
return
|
572
|
+
end
|
453
573
|
|
454
574
|
FileUtils.rm_f(temp_blocks_file_path)
|
455
575
|
|
456
576
|
clear_required_file
|
457
577
|
end
|
458
578
|
|
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
|
592
|
+
|
459
593
|
## Determines the correct filename to use for searching files
|
460
594
|
#
|
461
595
|
def determine_filename(specified_filename: nil, specified_folder: nil, default_filename: nil,
|
@@ -463,7 +597,8 @@ module MarkdownExec
|
|
463
597
|
if specified_filename&.present?
|
464
598
|
return specified_filename if specified_filename.start_with?('/')
|
465
599
|
|
466
|
-
File.join(specified_folder || default_folder,
|
600
|
+
File.join(specified_folder || default_folder,
|
601
|
+
specified_filename)
|
467
602
|
elsif specified_folder&.present?
|
468
603
|
File.join(specified_folder,
|
469
604
|
filetree ? @options[:md_filename_match] : @options[:md_filename_glob])
|
@@ -485,7 +620,7 @@ module MarkdownExec
|
|
485
620
|
command_execute(
|
486
621
|
opts,
|
487
622
|
required_lines.flatten.join("\n"),
|
488
|
-
args: opts.fetch(:
|
623
|
+
args: opts.fetch(:s_pass_args, [])
|
489
624
|
)
|
490
625
|
initialize_and_save_execution_output
|
491
626
|
output_execution_summary
|
@@ -495,23 +630,26 @@ module MarkdownExec
|
|
495
630
|
# Reports and executes block logic
|
496
631
|
def execute_block_logic(files)
|
497
632
|
@options[:filename] = select_document_if_multiple(files)
|
498
|
-
select_approve_and_execute_block({
|
499
|
-
|
500
|
-
struct: true
|
501
|
-
})
|
633
|
+
select_approve_and_execute_block({ bash: true,
|
634
|
+
struct: true })
|
502
635
|
end
|
503
636
|
|
504
637
|
## Executes the block specified in the options
|
505
638
|
#
|
506
639
|
def execute_block_with_error_handling(rest)
|
507
640
|
finalize_cli_argument_processing(rest)
|
508
|
-
|
641
|
+
@options[:s_cli_rest] = rest
|
642
|
+
execute_code_block_based_on_options(@options)
|
509
643
|
rescue FileMissingError => err
|
510
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
|
511
649
|
end
|
512
650
|
|
513
651
|
# Main method to execute a block based on options and block_name
|
514
|
-
def execute_code_block_based_on_options(options
|
652
|
+
def execute_code_block_based_on_options(options)
|
515
653
|
options = calculated_options.merge(options)
|
516
654
|
update_options(options, over: false)
|
517
655
|
|
@@ -519,7 +657,8 @@ module MarkdownExec
|
|
519
657
|
doc_glob: -> { fout options[:md_filename_glob] },
|
520
658
|
list_blocks: lambda do
|
521
659
|
fout_list (files.map do |file|
|
522
|
-
|
660
|
+
menu_with_block_labels(filename: file,
|
661
|
+
struct: true)
|
523
662
|
end).flatten(1)
|
524
663
|
end,
|
525
664
|
list_default_yaml: -> { fout_list list_default_yaml },
|
@@ -569,15 +708,6 @@ module MarkdownExec
|
|
569
708
|
false
|
570
709
|
end
|
571
710
|
|
572
|
-
##
|
573
|
-
# Determines the types of blocks to select based on the filter.
|
574
|
-
#
|
575
|
-
def filter_block_types
|
576
|
-
## return type of blocks to select
|
577
|
-
#
|
578
|
-
%i[blocks line]
|
579
|
-
end
|
580
|
-
|
581
711
|
## post-parse options configuration
|
582
712
|
#
|
583
713
|
def finalize_cli_argument_processing(rest)
|
@@ -599,6 +729,15 @@ module MarkdownExec
|
|
599
729
|
@options[:block_name] = block_name if block_name.present?
|
600
730
|
end
|
601
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
|
+
|
602
741
|
## summarize blocks
|
603
742
|
#
|
604
743
|
def get_block_summary(call_options, fcb)
|
@@ -612,9 +751,12 @@ module MarkdownExec
|
|
612
751
|
else
|
613
752
|
fcb.title
|
614
753
|
end
|
615
|
-
bm = extract_named_captures_from_option(titlexcall,
|
616
|
-
|
617
|
-
fcb.
|
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])
|
618
760
|
|
619
761
|
shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
|
620
762
|
fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall
|
@@ -626,22 +768,13 @@ module MarkdownExec
|
|
626
768
|
fcb
|
627
769
|
end
|
628
770
|
|
629
|
-
##
|
630
|
-
# Handles errors that occur during the block listing process.
|
631
|
-
#
|
632
|
-
def handle_error(err)
|
633
|
-
warn(error = "ERROR ** MarkParse.list_blocks_in_file(); #{err.inspect}")
|
634
|
-
warn(caller[0..4])
|
635
|
-
raise StandardError, error
|
636
|
-
end
|
637
|
-
|
638
771
|
# Handles the link-back operation.
|
639
772
|
#
|
640
773
|
# @param opts [Hash] Configuration options hash.
|
641
|
-
# @return [Array<Symbol, String>] A tuple containing a
|
774
|
+
# @return [Array<Symbol, String>] A tuple containing a LoadFile flag and an empty string.
|
642
775
|
def handle_back_link(opts)
|
643
776
|
history_state_pop(opts)
|
644
|
-
[
|
777
|
+
[LoadFile::Load, '']
|
645
778
|
end
|
646
779
|
|
647
780
|
# Handles the execution and display of remainder blocks from a selected menu item.
|
@@ -649,18 +782,27 @@ module MarkdownExec
|
|
649
782
|
# @param mdoc [Object] Document object containing code blocks.
|
650
783
|
# @param opts [Hash] Configuration options hash.
|
651
784
|
# @param selected [Hash] Selected item from the menu.
|
652
|
-
# @return [Array<Symbol, String>] A tuple containing a
|
785
|
+
# @return [Array<Symbol, String>] A tuple containing a LoadFile flag and an empty string.
|
653
786
|
# @note The function can prompt the user for approval before executing code if opts[:user_must_approve] is true.
|
654
787
|
def handle_remainder_blocks(mdoc, opts, selected)
|
655
|
-
required_lines = collect_required_code_lines(mdoc, selected,
|
788
|
+
required_lines = collect_required_code_lines(mdoc, selected,
|
789
|
+
opts: opts)
|
656
790
|
if opts[:output_script] || opts[:user_must_approve]
|
657
791
|
display_required_code(opts, required_lines)
|
658
792
|
end
|
659
|
-
allow = opts[:user_must_approve]
|
660
|
-
|
661
|
-
|
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
|
662
804
|
|
663
|
-
[
|
805
|
+
[LoadFile::Reuse, '']
|
664
806
|
end
|
665
807
|
|
666
808
|
# Handles the link-shell operation.
|
@@ -668,11 +810,11 @@ module MarkdownExec
|
|
668
810
|
# @param opts [Hash] Configuration options hash.
|
669
811
|
# @param body [Array<String>] The body content.
|
670
812
|
# @param mdoc [Object] Document object containing code blocks.
|
671
|
-
# @return [Array<Symbol, String>] A tuple containing a
|
813
|
+
# @return [Array<Symbol, String>] A tuple containing a LoadFile flag and a block name.
|
672
814
|
def handle_shell_link(opts, body, mdoc)
|
673
815
|
data = body.present? ? YAML.load(body.join("\n")) : {}
|
674
816
|
data_file = data.fetch('file', nil)
|
675
|
-
return [
|
817
|
+
return [LoadFile::Reuse, ''] unless data_file
|
676
818
|
|
677
819
|
history_state_push(mdoc, data_file, opts)
|
678
820
|
|
@@ -680,18 +822,19 @@ module MarkdownExec
|
|
680
822
|
ENV[var[0]] = var[1].to_s
|
681
823
|
end
|
682
824
|
|
683
|
-
[
|
825
|
+
[LoadFile::Load, data.fetch('block', '')]
|
684
826
|
end
|
685
827
|
|
686
828
|
# Handles options for the shell.
|
687
829
|
#
|
688
830
|
# @param opts [Hash] Configuration options hash.
|
689
831
|
# @param selected [Hash] Selected item from the menu.
|
690
|
-
# @return [Array<Symbol, String>] A tuple containing a
|
691
|
-
def handle_shell_opts(opts, selected)
|
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)
|
692
834
|
data = YAML.load(selected[:body].join("\n"))
|
693
835
|
data.each_key do |key|
|
694
|
-
opts[key.to_sym] = value = data[key]
|
836
|
+
opts[key.to_sym] = value = data[key]
|
837
|
+
tgt2[key.to_sym] = value if tgt2
|
695
838
|
next unless opts[:menu_opts_set_format].present?
|
696
839
|
|
697
840
|
print format(
|
@@ -700,7 +843,7 @@ module MarkdownExec
|
|
700
843
|
value: value }
|
701
844
|
).send(opts[:menu_opts_set_color].to_sym)
|
702
845
|
end
|
703
|
-
[
|
846
|
+
[LoadFile::Reuse, '']
|
704
847
|
end
|
705
848
|
|
706
849
|
# Handles reading and processing lines from a given IO stream
|
@@ -710,7 +853,8 @@ module MarkdownExec
|
|
710
853
|
def handle_stream(opts, stream, file_type, swap: false)
|
711
854
|
Thread.new do
|
712
855
|
until (line = stream.gets).nil?
|
713
|
-
@execute_files[file_type] =
|
856
|
+
@execute_files[file_type] =
|
857
|
+
@execute_files[file_type] + [line.strip]
|
714
858
|
print line if opts[:output_stdout]
|
715
859
|
yield line if block_given?
|
716
860
|
end
|
@@ -753,7 +897,8 @@ module MarkdownExec
|
|
753
897
|
#
|
754
898
|
def initialize_and_parse_cli_options
|
755
899
|
@options = base_options
|
756
|
-
read_configuration_file!(@options,
|
900
|
+
read_configuration_file!(@options,
|
901
|
+
".#{MarkdownExec::APP_NAME.downcase}.yml")
|
757
902
|
|
758
903
|
@option_parser = OptionParser.new do |opts|
|
759
904
|
executable_name = File.basename($PROGRAM_NAME)
|
@@ -771,7 +916,7 @@ module MarkdownExec
|
|
771
916
|
@option_parser.environment
|
772
917
|
|
773
918
|
rest = @option_parser.parse!(arguments_for_mde)
|
774
|
-
@options[:
|
919
|
+
@options[:s_pass_args] = ARGV[rest.count + 1..]
|
775
920
|
|
776
921
|
rest
|
777
922
|
end
|
@@ -781,7 +926,8 @@ module MarkdownExec
|
|
781
926
|
|
782
927
|
@options[:logged_stdout_filename] =
|
783
928
|
SavedAsset.stdout_name(blockname: @options[:block_name],
|
784
|
-
filename: File.basename(@options[:filename],
|
929
|
+
filename: File.basename(@options[:filename],
|
930
|
+
'.*'),
|
785
931
|
prefix: @options[:logged_stdout_filename_prefix],
|
786
932
|
time: Time.now.utc)
|
787
933
|
|
@@ -810,83 +956,56 @@ module MarkdownExec
|
|
810
956
|
|
811
957
|
state = initialize_state(opts)
|
812
958
|
|
813
|
-
# get type of messages to select
|
814
959
|
selected_messages = yield :filter
|
815
960
|
|
816
961
|
cfile.readlines(opts[:filename]).each do |line|
|
817
962
|
next unless line
|
818
963
|
|
819
|
-
update_line_and_block_state(line, state, opts, selected_messages,
|
964
|
+
update_line_and_block_state(line, state, opts, selected_messages,
|
965
|
+
&block)
|
820
966
|
end
|
821
967
|
end
|
822
968
|
|
823
969
|
##
|
824
|
-
# Returns a
|
825
|
-
# The
|
826
|
-
#
|
827
|
-
# @
|
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
|
-
if opts[:menu_divider_match].present? &&
|
862
|
-
(mbody = fcb.body[0].match opts[:menu_divider_match])
|
863
|
-
if use_chrome
|
864
|
-
blocks.push FCB.new(
|
865
|
-
{ chrome: true,
|
866
|
-
disabled: '',
|
867
|
-
dname: format(opts[:menu_divider_format],
|
868
|
-
mbody[:name]).send(opts[:menu_divider_color].to_sym),
|
869
|
-
oname: mbody[:name] }
|
870
|
-
)
|
871
|
-
end
|
872
|
-
elsif opts[:menu_task_match].present? &&
|
873
|
-
(fcb.body[0].match opts[:menu_task_match])
|
874
|
-
if use_chrome
|
875
|
-
blocks.push FCB.new(
|
876
|
-
{ chrome: true,
|
877
|
-
disabled: '',
|
878
|
-
dname: format(
|
879
|
-
opts[:menu_task_format],
|
880
|
-
$~.named_captures.transform_keys(&:to_sym)
|
881
|
-
).send(opts[:menu_task_color].to_sym),
|
882
|
-
oname: format(
|
883
|
-
opts[:menu_task_format],
|
884
|
-
$~.named_captures.transform_keys(&:to_sym)
|
885
|
-
) }
|
886
|
-
)
|
887
|
-
end
|
970
|
+
# Returns a lambda expression based on the given procname.
|
971
|
+
# @param procname [String] The name of the process to generate a lambda for.
|
972
|
+
# @param options [Hash] The options hash, necessary for some lambdas to access.
|
973
|
+
# @return [Lambda] The corresponding lambda expression.
|
974
|
+
def lambda_for_procname(procname, options)
|
975
|
+
case procname
|
976
|
+
when 'debug'
|
977
|
+
lambda { |value|
|
978
|
+
tap_config value: value
|
979
|
+
}
|
980
|
+
when 'exit'
|
981
|
+
->(_) { exit }
|
982
|
+
when 'help'
|
983
|
+
lambda { |_|
|
984
|
+
fout menu_help
|
985
|
+
exit
|
986
|
+
}
|
987
|
+
when 'path'
|
988
|
+
->(value) { read_configuration_file!(options, value) }
|
989
|
+
when 'show_config'
|
990
|
+
lambda { |_|
|
991
|
+
finalize_cli_argument_processing(options)
|
992
|
+
fout options.sort_by_key.to_yaml
|
993
|
+
}
|
994
|
+
when 'val_as_bool'
|
995
|
+
lambda { |value|
|
996
|
+
value.instance_of?(::String) ? (value.chomp != '0') : value
|
997
|
+
}
|
998
|
+
when 'val_as_int'
|
999
|
+
->(value) { value.to_i }
|
1000
|
+
when 'val_as_str'
|
1001
|
+
->(value) { value.to_s }
|
1002
|
+
when 'version'
|
1003
|
+
lambda { |_|
|
1004
|
+
fout MarkdownExec::VERSION
|
1005
|
+
exit
|
1006
|
+
}
|
888
1007
|
else
|
889
|
-
|
1008
|
+
procname
|
890
1009
|
end
|
891
1010
|
end
|
892
1011
|
|
@@ -942,38 +1061,68 @@ module MarkdownExec
|
|
942
1061
|
#
|
943
1062
|
def list_named_blocks_in_file(call_options = {}, &options_block)
|
944
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
|
945
1081
|
|
946
|
-
|
947
|
-
|
948
|
-
|
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)
|
949
1091
|
end
|
950
|
-
|
1092
|
+
[menu_blocks, mdoc]
|
951
1093
|
end
|
952
1094
|
|
953
1095
|
## Handles the file loading and returns the blocks in the file and MDoc instance
|
954
1096
|
#
|
955
|
-
def
|
956
|
-
blocks_in_file =
|
957
|
-
|
958
|
-
|
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)
|
959
1103
|
end
|
1104
|
+
|
960
1105
|
blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
|
1106
|
+
add_menu_chrome_blocks!(blocks_menu)
|
961
1107
|
[blocks_in_file, blocks_menu, mdoc]
|
962
1108
|
end
|
963
1109
|
|
964
|
-
def
|
965
|
-
|
966
|
-
|
967
|
-
|
968
|
-
|
969
|
-
|
970
|
-
|
971
|
-
|
972
|
-
|
973
|
-
|
974
|
-
|
975
|
-
|
976
|
-
|
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)
|
977
1126
|
end
|
978
1127
|
|
979
1128
|
def menu_export(data = menu_for_optparse)
|
@@ -993,7 +1142,13 @@ module MarkdownExec
|
|
993
1142
|
when :line
|
994
1143
|
if options[:menu_divider_match] &&
|
995
1144
|
(mbody = fcb.body[0].match(options[:menu_divider_match]))
|
996
|
-
menu.push FCB.new({ dname: mbody[:name], oname: mbody[:name],
|
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: '' })
|
997
1152
|
end
|
998
1153
|
when :blocks
|
999
1154
|
menu += [fcb.oname]
|
@@ -1002,57 +1157,49 @@ module MarkdownExec
|
|
1002
1157
|
menu
|
1003
1158
|
end
|
1004
1159
|
|
1005
|
-
|
1006
|
-
#
|
1160
|
+
##
|
1161
|
+
# Generates a menu suitable for OptionParser from the menu items defined in YAML format.
|
1162
|
+
# @return [Array<Hash>] The array of option hashes for OptionParser.
|
1007
1163
|
def menu_for_optparse
|
1008
1164
|
menu_from_yaml.map do |menu_item|
|
1009
1165
|
menu_item.merge(
|
1010
|
-
|
1011
|
-
|
1012
|
-
proccode: case menu_item[:procname]
|
1013
|
-
when 'debug'
|
1014
|
-
lambda { |value|
|
1015
|
-
tap_config value: value
|
1016
|
-
}
|
1017
|
-
when 'exit'
|
1018
|
-
lambda { |_|
|
1019
|
-
exit
|
1020
|
-
}
|
1021
|
-
when 'help'
|
1022
|
-
lambda { |_|
|
1023
|
-
fout menu_help
|
1024
|
-
exit
|
1025
|
-
}
|
1026
|
-
when 'path'
|
1027
|
-
lambda { |value|
|
1028
|
-
read_configuration_file!(options, value)
|
1029
|
-
}
|
1030
|
-
when 'show_config'
|
1031
|
-
lambda { |_|
|
1032
|
-
finalize_cli_argument_processing(options)
|
1033
|
-
fout options.sort_by_key.to_yaml
|
1034
|
-
}
|
1035
|
-
when 'val_as_bool'
|
1036
|
-
lambda { |value|
|
1037
|
-
value.instance_of?(::String) ? (value.chomp != '0') : value
|
1038
|
-
}
|
1039
|
-
when 'val_as_int'
|
1040
|
-
->(value) { value.to_i }
|
1041
|
-
when 'val_as_str'
|
1042
|
-
->(value) { value.to_s }
|
1043
|
-
when 'version'
|
1044
|
-
lambda { |_|
|
1045
|
-
fout MarkdownExec::VERSION
|
1046
|
-
exit
|
1047
|
-
}
|
1048
|
-
else
|
1049
|
-
menu_item[:procname]
|
1050
|
-
end
|
1051
|
-
}
|
1166
|
+
opt_name: menu_item[:opt_name]&.to_sym,
|
1167
|
+
proccode: lambda_for_procname(menu_item[:procname], options)
|
1052
1168
|
)
|
1053
1169
|
end
|
1054
1170
|
end
|
1055
1171
|
|
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
|
+
|
1056
1203
|
def menu_help
|
1057
1204
|
@option_parser.help
|
1058
1205
|
end
|
@@ -1062,7 +1209,9 @@ module MarkdownExec
|
|
1062
1209
|
end
|
1063
1210
|
|
1064
1211
|
def menu_option_append(opts, options, item)
|
1065
|
-
|
1212
|
+
unless item[:long_name].present? || item[:short_name].present?
|
1213
|
+
return
|
1214
|
+
end
|
1066
1215
|
|
1067
1216
|
opts.on(*[
|
1068
1217
|
# - long name
|
@@ -1075,7 +1224,9 @@ module MarkdownExec
|
|
1075
1224
|
|
1076
1225
|
# - description and default
|
1077
1226
|
[item[:description],
|
1078
|
-
(
|
1227
|
+
(if item[:default].present?
|
1228
|
+
"[#{value_for_cli item[:default]}]"
|
1229
|
+
end)].compact.join(' '),
|
1079
1230
|
|
1080
1231
|
# apply proccode, if present, to value
|
1081
1232
|
# save value to options hash if option is named
|
@@ -1088,6 +1239,30 @@ module MarkdownExec
|
|
1088
1239
|
].compact)
|
1089
1240
|
end
|
1090
1241
|
|
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
|
+
|
1091
1266
|
# :reek:ControlParameter
|
1092
1267
|
def optsmerge(call_options = {}, options_block = nil)
|
1093
1268
|
class_call_options = @options.merge(call_options || {})
|
@@ -1108,7 +1283,10 @@ module MarkdownExec
|
|
1108
1283
|
|
1109
1284
|
[['Script', :saved_filespec],
|
1110
1285
|
['StdOut', :logged_stdout_filespec]].each do |label, name|
|
1111
|
-
|
1286
|
+
if @options[name]
|
1287
|
+
oq << [label, @options[name],
|
1288
|
+
DISPLAY_LEVEL_ADMIN]
|
1289
|
+
end
|
1112
1290
|
end
|
1113
1291
|
|
1114
1292
|
oq.map do |label, value, level|
|
@@ -1139,7 +1317,9 @@ module MarkdownExec
|
|
1139
1317
|
def prepare_blocks_menu(blocks_in_file, opts)
|
1140
1318
|
# next if fcb.fetch(:disabled, false)
|
1141
1319
|
# next unless fcb.fetch(:name, '').present?
|
1142
|
-
blocks_in_file.map do |fcb|
|
1320
|
+
replace_consecutive_blanks(blocks_in_file).map do |fcb|
|
1321
|
+
next if Filter.prepared_not_in_menu?(opts, fcb)
|
1322
|
+
|
1143
1323
|
fcb.merge!(
|
1144
1324
|
name: fcb.dname,
|
1145
1325
|
label: BlockLabel.make(
|
@@ -1165,7 +1345,7 @@ module MarkdownExec
|
|
1165
1345
|
fcb.oname = fcb.dname = fcb.title || ''
|
1166
1346
|
return unless fcb.body
|
1167
1347
|
|
1168
|
-
|
1348
|
+
update_title_from_body(fcb)
|
1169
1349
|
|
1170
1350
|
if block &&
|
1171
1351
|
selected_messages.include?(:blocks) &&
|
@@ -1183,6 +1363,13 @@ module MarkdownExec
|
|
1183
1363
|
block.call(:line, fcb)
|
1184
1364
|
end
|
1185
1365
|
|
1366
|
+
class MenuOptions
|
1367
|
+
YES = 1
|
1368
|
+
NO = 2
|
1369
|
+
SCRIPT_TO_CLIPBOARD = 3
|
1370
|
+
SAVE_SCRIPT = 4
|
1371
|
+
end
|
1372
|
+
|
1186
1373
|
##
|
1187
1374
|
# Presents a menu to the user for approving an action and performs additional tasks based on the selection.
|
1188
1375
|
# The function provides options for approval, rejection, copying data to clipboard, or saving data to a file.
|
@@ -1200,44 +1387,40 @@ module MarkdownExec
|
|
1200
1387
|
##
|
1201
1388
|
def prompt_for_user_approval(opts, required_lines)
|
1202
1389
|
# Present a selection menu for user approval.
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
1206
|
-
menu.
|
1207
|
-
menu.choice opts[:
|
1208
|
-
menu.choice opts[:
|
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
|
1209
1399
|
end
|
1210
1400
|
|
1211
|
-
if sel ==
|
1401
|
+
if sel == MenuOptions::SCRIPT_TO_CLIPBOARD
|
1212
1402
|
copy_to_clipboard(required_lines)
|
1213
|
-
elsif sel ==
|
1403
|
+
elsif sel == MenuOptions::SAVE_SCRIPT
|
1214
1404
|
save_to_file(opts, required_lines)
|
1215
1405
|
end
|
1216
1406
|
|
1217
|
-
sel ==
|
1218
|
-
|
1219
|
-
|
1220
|
-
## insert back option at head or tail
|
1221
|
-
#
|
1222
|
-
## Adds a back option at the head or tail of a menu
|
1223
|
-
#
|
1224
|
-
def prompt_menu_add_back(items, label)
|
1225
|
-
return items unless @options[:menu_with_back] && history_state_exist?
|
1226
|
-
|
1227
|
-
state = history_state_partition(@options)
|
1228
|
-
@hs_curr = state[:unit]
|
1229
|
-
@hs_rest = state[:rest]
|
1230
|
-
@options[:menu_back_at_top] ? [label] + items : items + [label]
|
1407
|
+
sel == MenuOptions::YES
|
1408
|
+
rescue TTY::Reader::InputInterrupt
|
1409
|
+
exit 1
|
1231
1410
|
end
|
1232
1411
|
|
1233
|
-
|
1234
|
-
|
1235
|
-
|
1236
|
-
|
1237
|
-
|
1238
|
-
|
1239
|
-
|
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]
|
1240
1420
|
end
|
1421
|
+
sel == opts[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
|
1422
|
+
rescue TTY::Reader::InputInterrupt
|
1423
|
+
exit 1
|
1241
1424
|
end
|
1242
1425
|
|
1243
1426
|
# :reek:UtilityFunction ### temp
|
@@ -1256,7 +1439,9 @@ module MarkdownExec
|
|
1256
1439
|
temp_blocks = []
|
1257
1440
|
|
1258
1441
|
temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
|
1259
|
-
|
1442
|
+
if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
|
1443
|
+
return temp_blocks
|
1444
|
+
end
|
1260
1445
|
|
1261
1446
|
if File.exist?(temp_blocks_file_path)
|
1262
1447
|
temp_blocks = File.readlines(temp_blocks_file_path, chomp: true)
|
@@ -1265,6 +1450,24 @@ module MarkdownExec
|
|
1265
1450
|
temp_blocks
|
1266
1451
|
end
|
1267
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
|
1470
|
+
|
1268
1471
|
def run
|
1269
1472
|
clear_required_file
|
1270
1473
|
execute_block_with_error_handling(initialize_and_parse_cli_options)
|
@@ -1282,7 +1485,15 @@ module MarkdownExec
|
|
1282
1485
|
|
1283
1486
|
saved_name_split filename
|
1284
1487
|
@options[:save_executed_script] = false
|
1285
|
-
select_approve_and_execute_block
|
1488
|
+
select_approve_and_execute_block
|
1489
|
+
end
|
1490
|
+
|
1491
|
+
def safeval(str)
|
1492
|
+
eval(str)
|
1493
|
+
rescue StandardError
|
1494
|
+
warn $!
|
1495
|
+
binding.pry if $tap_enable
|
1496
|
+
raise StandardError, $!
|
1286
1497
|
end
|
1287
1498
|
|
1288
1499
|
def save_to_file(opts, required_lines)
|
@@ -1309,39 +1520,48 @@ module MarkdownExec
|
|
1309
1520
|
# @param call_options [Hash] Initial options for the method.
|
1310
1521
|
# @param options_block [Block] Block of options to be merged with call_options.
|
1311
1522
|
# @return [Nil] Returns nil if no code block is selected or an error occurs.
|
1312
|
-
def select_approve_and_execute_block(call_options,
|
1313
|
-
|
1314
|
-
|
1315
|
-
|
1316
|
-
|
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
|
1317
1530
|
|
1318
1531
|
loop do
|
1319
1532
|
loop do
|
1320
|
-
opts
|
1321
|
-
|
1322
|
-
|
1323
|
-
|
1324
|
-
|
1325
|
-
|
1326
|
-
|
1327
|
-
|
1328
|
-
|
1329
|
-
|
1330
|
-
|
1331
|
-
|
1332
|
-
|
1333
|
-
|
1334
|
-
|
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
|
1335
1553
|
end
|
1336
1554
|
|
1337
|
-
|
1338
|
-
|
1339
|
-
|
1340
|
-
|
1341
|
-
|
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
|
1342
1560
|
break unless repeat_menu
|
1343
1561
|
end
|
1344
|
-
break if load_file
|
1562
|
+
break if load_file == LoadFile::Reuse
|
1563
|
+
|
1564
|
+
repeat_menu = next_block_name_from_command_line_arguments(base_opts)
|
1345
1565
|
end
|
1346
1566
|
rescue StandardError => err
|
1347
1567
|
warn(error = "ERROR ** MarkParse.select_approve_and_execute_block(); #{err.inspect}")
|
@@ -1371,21 +1591,24 @@ module MarkdownExec
|
|
1371
1591
|
# Presents a TTY prompt to select an option or exit, returns metadata including option and selected
|
1372
1592
|
def select_option_with_metadata(prompt_text, items, opts = {})
|
1373
1593
|
selection = @prompt.select(prompt_text,
|
1374
|
-
|
1375
|
-
prompt_menu_add_back(
|
1376
|
-
items,
|
1377
|
-
opts[:menu_option_back_name]
|
1378
|
-
),
|
1379
|
-
opts[:menu_option_exit_name]
|
1380
|
-
),
|
1594
|
+
items,
|
1381
1595
|
opts.merge(filter: true))
|
1382
|
-
|
1383
|
-
|
1384
|
-
|
1385
|
-
|
1386
|
-
|
1387
|
-
|
1388
|
-
|
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
|
1389
1612
|
end
|
1390
1613
|
|
1391
1614
|
def select_recent_output
|
@@ -1417,21 +1640,13 @@ module MarkdownExec
|
|
1417
1640
|
|
1418
1641
|
saved_name_split(filename)
|
1419
1642
|
|
1420
|
-
select_approve_and_execute_block({
|
1421
|
-
bash: true,
|
1643
|
+
select_approve_and_execute_block({ bash: true,
|
1422
1644
|
save_executed_script: false,
|
1423
|
-
struct: true
|
1424
|
-
})
|
1645
|
+
struct: true })
|
1425
1646
|
end
|
1426
1647
|
|
1427
|
-
|
1428
|
-
|
1429
|
-
return unless fcb.title.nil? || fcb.title.empty?
|
1430
|
-
|
1431
|
-
fcb.title = (fcb&.body || []).join(' ').gsub(/ +/, ' ')[0..64]
|
1432
|
-
end
|
1433
|
-
|
1434
|
-
def start_fenced_block(opts, line, headings, fenced_start_extended_regex)
|
1648
|
+
def start_fenced_block(opts, line, headings,
|
1649
|
+
fenced_start_extended_regex)
|
1435
1650
|
fcb_title_groups = line.match(fenced_start_extended_regex).named_captures.sym_keys
|
1436
1651
|
rest = fcb_title_groups.fetch(:rest, '')
|
1437
1652
|
|
@@ -1464,7 +1679,11 @@ module MarkdownExec
|
|
1464
1679
|
end
|
1465
1680
|
|
1466
1681
|
def tty_prompt_without_disabled_symbol
|
1467
|
-
TTY::Prompt.new(interrupt:
|
1682
|
+
TTY::Prompt.new(interrupt: lambda {
|
1683
|
+
puts;
|
1684
|
+
raise TTY::Reader::InputInterrupt
|
1685
|
+
},
|
1686
|
+
symbols: { cross: ' ' })
|
1468
1687
|
end
|
1469
1688
|
|
1470
1689
|
##
|
@@ -1511,7 +1730,8 @@ module MarkdownExec
|
|
1511
1730
|
#
|
1512
1731
|
# @return [Void] The function modifies the `state` and `selected_messages` arguments in place.
|
1513
1732
|
##
|
1514
|
-
def update_line_and_block_state(line, state, opts, selected_messages,
|
1733
|
+
def update_line_and_block_state(line, state, opts, selected_messages,
|
1734
|
+
&block)
|
1515
1735
|
if opts[:menu_blocks_with_headings]
|
1516
1736
|
state[:headings] =
|
1517
1737
|
update_document_headings(line, state[:headings], opts)
|
@@ -1519,7 +1739,8 @@ module MarkdownExec
|
|
1519
1739
|
|
1520
1740
|
if line.match(state[:fenced_start_and_end_regex])
|
1521
1741
|
if state[:in_fenced_block]
|
1522
|
-
process_fenced_block(state[:fcb], opts, selected_messages,
|
1742
|
+
process_fenced_block(state[:fcb], opts, selected_messages,
|
1743
|
+
&block)
|
1523
1744
|
state[:in_fenced_block] = false
|
1524
1745
|
else
|
1525
1746
|
state[:fcb] =
|
@@ -1545,26 +1766,59 @@ module MarkdownExec
|
|
1545
1766
|
@options
|
1546
1767
|
end
|
1547
1768
|
|
1548
|
-
|
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
|
1549
1792
|
#
|
1550
|
-
def wait_for_user_selection(blocks_in_file, blocks_menu, default,
|
1793
|
+
def wait_for_user_selection(blocks_in_file, blocks_menu, default,
|
1794
|
+
opts)
|
1551
1795
|
pt = opts[:prompt_select_block].to_s
|
1552
1796
|
bm = prepare_blocks_menu(blocks_menu, opts)
|
1553
|
-
return [nil,
|
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
|
1554
1804
|
|
1555
|
-
obj = select_option_with_metadata(pt, bm,
|
1556
|
-
default: default,
|
1805
|
+
obj = select_option_with_metadata(pt, bm, o2.merge(
|
1557
1806
|
per_page: opts[:select_page_height]
|
1558
1807
|
))
|
1559
|
-
|
1560
|
-
|
1561
|
-
|
1562
|
-
|
1563
|
-
|
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]
|
1564
1814
|
else
|
1565
|
-
|
1566
|
-
[label_block.oname, :continue]
|
1815
|
+
[obj, MenuState::CONTINUE]
|
1567
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
|
1568
1822
|
end
|
1569
1823
|
|
1570
1824
|
# Handles the core logic for generating the command file's metadata and content.
|
@@ -1581,7 +1835,8 @@ module MarkdownExec
|
|
1581
1835
|
|
1582
1836
|
@execute_script_filespec =
|
1583
1837
|
@options[:saved_filespec] =
|
1584
|
-
File.join opts[:saved_script_folder],
|
1838
|
+
File.join opts[:saved_script_folder],
|
1839
|
+
opts[:saved_script_filename]
|
1585
1840
|
|
1586
1841
|
shebang = if @options[:shebang]&.present?
|
1587
1842
|
"#{@options[:shebang]} #{@options[:shell]}\n"
|
@@ -1625,7 +1880,6 @@ module MarkdownExec
|
|
1625
1880
|
)[:code]).join("\n")
|
1626
1881
|
|
1627
1882
|
Dir::Tmpname.create(self.class.to_s) do |path|
|
1628
|
-
pp path
|
1629
1883
|
File.write(path, code_blocks)
|
1630
1884
|
ENV['MDE_LINK_REQUIRED_FILE'] = path
|
1631
1885
|
end
|
@@ -1645,7 +1899,7 @@ if $PROGRAM_NAME == __FILE__
|
|
1645
1899
|
|
1646
1900
|
def test_calling_execute_approved_block_calls_command_execute_with_argument_args_value
|
1647
1901
|
pigeon = 'E'
|
1648
|
-
obj = {
|
1902
|
+
obj = { s_pass_args: pigeon }
|
1649
1903
|
|
1650
1904
|
c = MarkdownExec::MarkParse.new
|
1651
1905
|
|
@@ -1665,15 +1919,17 @@ if $PROGRAM_NAME == __FILE__
|
|
1665
1919
|
end
|
1666
1920
|
|
1667
1921
|
def test_set_fcb_title
|
1668
|
-
# sample input and output data for testing
|
1922
|
+
# sample input and output data for testing update_title_from_body method
|
1669
1923
|
input_output_data = [
|
1670
1924
|
{
|
1671
1925
|
input: FCB.new(title: nil, body: ["puts 'Hello, world!'"]),
|
1672
1926
|
output: "puts 'Hello, world!'"
|
1673
1927
|
},
|
1674
1928
|
{
|
1675
|
-
input: FCB.new(title: '',
|
1676
|
-
|
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"
|
1677
1933
|
},
|
1678
1934
|
{
|
1679
1935
|
input: FCB.new(title: 'foo', body: %w[bar baz]),
|
@@ -1686,10 +1942,20 @@ if $PROGRAM_NAME == __FILE__
|
|
1686
1942
|
input_output_data.each do |data|
|
1687
1943
|
input = data[:input]
|
1688
1944
|
output = data[:output]
|
1689
|
-
@mark_parse.
|
1945
|
+
@mark_parse.update_title_from_body(input)
|
1690
1946
|
assert_equal output, input.title
|
1691
1947
|
end
|
1692
1948
|
end
|
1693
1949
|
end
|
1694
|
-
|
1695
|
-
|
1950
|
+
|
1951
|
+
def test_select_block
|
1952
|
+
blocks = [block1, block2]
|
1953
|
+
menu = [m1, m2]
|
1954
|
+
|
1955
|
+
block, state = obj.select_block(blocks, menu, nil, {})
|
1956
|
+
|
1957
|
+
assert_equal block1, block
|
1958
|
+
assert_equal MenuState::CONTINUE, state
|
1959
|
+
end
|
1960
|
+
end # module MarkdownExec
|
1961
|
+
end # if
|