markdown_exec 2.0.5 → 2.0.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 +12 -5
- data/CHANGELOG.md +21 -0
- data/Gemfile.lock +1 -1
- data/bin/tab_completion.sh +15 -31
- data/examples/block_names.md +23 -0
- data/examples/linked.md +8 -0
- data/examples/linked_show.md +7 -0
- data/examples/load_code.md +10 -0
- data/lib/color_scheme.rb +65 -0
- data/lib/constants.rb +18 -19
- data/lib/find_files.rb +35 -39
- data/lib/hash_delegator.rb +829 -616
- data/lib/input_sequencer.rb +11 -4
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/markdown_exec.rb +16 -7
- data/lib/menu.src.yml +76 -19
- data/lib/menu.yml +68 -12
- metadata +6 -2
data/lib/hash_delegator.rb
CHANGED
@@ -200,7 +200,7 @@ module HashDelegatorSelf
|
|
200
200
|
|
201
201
|
# Determine the state of breaker based on was_using_cli and the block type
|
202
202
|
# true only when block_name is nil, block_name_from_cli is false, was_using_cli is true, and the block_state.block[:shell] equals BlockType::BASH. In all other scenarios, breaker is false.
|
203
|
-
breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block
|
203
|
+
breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block.fetch(:shell, nil) == BlockType::BASH
|
204
204
|
|
205
205
|
# Reset block_name_from_cli if the conditions are not met
|
206
206
|
block_name_from_cli ||= false
|
@@ -237,41 +237,17 @@ module HashDelegatorSelf
|
|
237
237
|
# @param str [String] The string to be evaluated.
|
238
238
|
# @return [Object] The result of evaluating the string.
|
239
239
|
def safeval(str)
|
240
|
-
# # Restricting to evaluate only expressions
|
241
|
-
# unless str.match?(/\A\s*\w+\s*[\+\-\*\/\=\%\&\|\<\>\!]+\s*\w+\s*\z/)
|
242
|
-
# error_handler('safeval') # 'Invalid expression'
|
243
|
-
# return
|
244
|
-
# end
|
245
|
-
|
246
|
-
# # Whitelisting allowed operations
|
247
|
-
# allowed_methods = %w[+ - * / == != < > <= >= && || % & |]
|
248
|
-
# unless allowed_methods.any? { |op| str.include?(op) }
|
249
|
-
# error_handler('safeval', 'Operation not allowed')
|
250
|
-
# return
|
251
|
-
# end
|
252
|
-
|
253
|
-
# # Sanitize input (example: removing potentially harmful characters)
|
254
|
-
# str = str.gsub(/[^0-9\+\-\*\/\(\)\<\>\!\=\%\&\|]/, '')
|
255
|
-
|
256
|
-
# Evaluate the sanitized string
|
257
240
|
result = nil
|
258
241
|
binding.eval("result = #{str}")
|
259
242
|
|
260
243
|
result
|
261
244
|
rescue StandardError # catches NameError, StandardError
|
245
|
+
pp $!, $@
|
246
|
+
pp "code: #{str}"
|
262
247
|
error_handler('safeval')
|
248
|
+
exit 1
|
263
249
|
end
|
264
250
|
|
265
|
-
# # Evaluates the given string as Ruby code and rescues any StandardErrors.
|
266
|
-
# # If an error occurs, it calls the error_handler method with 'safeval'.
|
267
|
-
# # @param str [String] The string to be evaluated.
|
268
|
-
# # @return [Object] The result of evaluating the string.
|
269
|
-
# def safeval(str)
|
270
|
-
# eval(str)
|
271
|
-
# rescue StandardError # catches NameError, StandardError
|
272
|
-
# error_handler('safeval')
|
273
|
-
# end
|
274
|
-
|
275
251
|
def set_file_permissions(file_path, chmod_value)
|
276
252
|
File.chmod(chmod_value, file_path)
|
277
253
|
end
|
@@ -311,11 +287,11 @@ module HashDelegatorSelf
|
|
311
287
|
File.write(
|
312
288
|
filespec,
|
313
289
|
["-STDOUT-\n",
|
314
|
-
format_execution_streams(ExecutionStreams::
|
290
|
+
format_execution_streams(ExecutionStreams::STD_OUT, files),
|
315
291
|
"-STDERR-\n",
|
316
|
-
format_execution_streams(ExecutionStreams::
|
292
|
+
format_execution_streams(ExecutionStreams::STD_ERR, files),
|
317
293
|
"-STDIN-\n",
|
318
|
-
format_execution_streams(ExecutionStreams::
|
294
|
+
format_execution_streams(ExecutionStreams::STD_IN, files),
|
319
295
|
"\n"].join
|
320
296
|
)
|
321
297
|
end
|
@@ -387,7 +363,7 @@ module PathUtils
|
|
387
363
|
# @param expression [String] The expression where a wildcard '*' is replaced by the path if it's not absolute.
|
388
364
|
# @return [String] The absolute path or the expression with the wildcard replaced by the path.
|
389
365
|
def self.resolve_path_or_substitute(path, expression)
|
390
|
-
if path.
|
366
|
+
if path.start_with?('/')
|
391
367
|
path
|
392
368
|
else
|
393
369
|
expression.gsub('*', path)
|
@@ -509,9 +485,21 @@ module MarkdownExec
|
|
509
485
|
history_state_partition
|
510
486
|
option_name = @delegate_object[:menu_option_back_name]
|
511
487
|
insert_at_top = @delegate_object[:menu_back_at_top]
|
488
|
+
when MenuState::EDIT
|
489
|
+
option_name = @delegate_object[:menu_option_edit_name]
|
490
|
+
insert_at_top = @delegate_object[:menu_load_at_top]
|
512
491
|
when MenuState::EXIT
|
513
492
|
option_name = @delegate_object[:menu_option_exit_name]
|
514
493
|
insert_at_top = @delegate_object[:menu_exit_at_top]
|
494
|
+
when MenuState::LOAD
|
495
|
+
option_name = @delegate_object[:menu_option_load_name]
|
496
|
+
insert_at_top = @delegate_object[:menu_load_at_top]
|
497
|
+
when MenuState::SAVE
|
498
|
+
option_name = @delegate_object[:menu_option_save_name]
|
499
|
+
insert_at_top = @delegate_object[:menu_load_at_top]
|
500
|
+
when MenuState::VIEW
|
501
|
+
option_name = @delegate_object[:menu_option_view_name]
|
502
|
+
insert_at_top = @delegate_object[:menu_load_at_top]
|
515
503
|
end
|
516
504
|
|
517
505
|
formatted_name = format(@delegate_object[:menu_link_format],
|
@@ -519,7 +507,7 @@ module MarkdownExec
|
|
519
507
|
chrome_block = FCB.new(
|
520
508
|
chrome: true,
|
521
509
|
dname: HashDelegator.new(@delegate_object).string_send_color(
|
522
|
-
formatted_name, :
|
510
|
+
formatted_name, :menu_chrome_color
|
523
511
|
),
|
524
512
|
oname: formatted_name
|
525
513
|
)
|
@@ -617,6 +605,18 @@ module MarkdownExec
|
|
617
605
|
HashDelegator.error_handler('blocks_from_nested_files')
|
618
606
|
end
|
619
607
|
|
608
|
+
def block_state_for_name_from_cli(block_name)
|
609
|
+
SelectedBlockMenuState.new(
|
610
|
+
@dml_blocks_in_file.find do |item|
|
611
|
+
item[:oname] == block_name
|
612
|
+
end&.merge(
|
613
|
+
block_name_from_cli: true,
|
614
|
+
block_name_from_ui: false
|
615
|
+
),
|
616
|
+
MenuState::CONTINUE
|
617
|
+
)
|
618
|
+
end
|
619
|
+
|
620
620
|
# private
|
621
621
|
|
622
622
|
def calc_logged_stdout_filename(block_name:)
|
@@ -725,14 +725,14 @@ module MarkdownExec
|
|
725
725
|
'-c', command,
|
726
726
|
@delegate_object[:filename],
|
727
727
|
*args) do |stdin, stdout, stderr, exec_thr|
|
728
|
-
handle_stream(stream: stdout, file_type: ExecutionStreams::
|
728
|
+
handle_stream(stream: stdout, file_type: ExecutionStreams::STD_OUT) do |line|
|
729
729
|
yield nil, line, nil, exec_thr if block_given?
|
730
730
|
end
|
731
|
-
handle_stream(stream: stderr, file_type: ExecutionStreams::
|
731
|
+
handle_stream(stream: stderr, file_type: ExecutionStreams::STD_ERR) do |line|
|
732
732
|
yield nil, nil, line, exec_thr if block_given?
|
733
733
|
end
|
734
734
|
|
735
|
-
in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::
|
735
|
+
in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::STD_IN) do |line|
|
736
736
|
stdin.puts(line)
|
737
737
|
yield line, nil, nil, exec_thr if block_given?
|
738
738
|
end
|
@@ -750,34 +750,17 @@ module MarkdownExec
|
|
750
750
|
@run_state.aborted_at = Time.now.utc
|
751
751
|
@run_state.error_message = err.message
|
752
752
|
@run_state.error = err
|
753
|
-
@run_state.files[ExecutionStreams::
|
753
|
+
@run_state.files[ExecutionStreams::STD_ERR] += [@run_state.error_message]
|
754
754
|
@fout.fout "Error ENOENT: #{err.inspect}"
|
755
755
|
rescue SignalException => err
|
756
756
|
# Handle SignalException
|
757
757
|
@run_state.aborted_at = Time.now.utc
|
758
758
|
@run_state.error_message = 'SIGTERM'
|
759
759
|
@run_state.error = err
|
760
|
-
@run_state.files[ExecutionStreams::
|
760
|
+
@run_state.files[ExecutionStreams::STD_ERR] += [@run_state.error_message]
|
761
761
|
@fout.fout "Error ENOENT: #{err.inspect}"
|
762
762
|
end
|
763
763
|
|
764
|
-
def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil)
|
765
|
-
if @delegate_object[:block_name].present?
|
766
|
-
block = all_blocks.find do |item|
|
767
|
-
item[:oname] == @delegate_object[:block_name]
|
768
|
-
end&.merge(block_name_from_ui: false)
|
769
|
-
else
|
770
|
-
block_state = wait_for_user_selected_block(all_blocks, menu_blocks,
|
771
|
-
default)
|
772
|
-
block = block_state.block&.merge(block_name_from_ui: true)
|
773
|
-
state = block_state.state
|
774
|
-
end
|
775
|
-
|
776
|
-
SelectedBlockMenuState.new(block, state)
|
777
|
-
rescue StandardError
|
778
|
-
HashDelegator.error_handler('load_cli_or_user_selected_block')
|
779
|
-
end
|
780
|
-
|
781
764
|
# This method is responsible for handling the execution of generic blocks in a markdown document.
|
782
765
|
# It collects the required code lines from the document and, depending on the configuration,
|
783
766
|
# may display the code for user approval before execution. It then executes the approved block.
|
@@ -799,7 +782,12 @@ module MarkdownExec
|
|
799
782
|
execute_required_lines(required_lines: required_lines, selected: selected) if allow_execution
|
800
783
|
|
801
784
|
link_state.block_name = nil
|
802
|
-
LoadFileLinkState.new(LoadFile::
|
785
|
+
LoadFileLinkState.new(LoadFile::REUSE, link_state)
|
786
|
+
end
|
787
|
+
|
788
|
+
# Check if the expression contains wildcard characters
|
789
|
+
def contains_wildcards?(expr)
|
790
|
+
expr.match(%r{\*|\?|\[})
|
803
791
|
end
|
804
792
|
|
805
793
|
def copy_to_clipboard(required_lines)
|
@@ -950,6 +938,312 @@ module MarkdownExec
|
|
950
938
|
@delegate_object[:logged_stdout_filespec])
|
951
939
|
end
|
952
940
|
|
941
|
+
# Select and execute a code block from a Markdown document.
|
942
|
+
#
|
943
|
+
# This method allows the user to interactively select a code block from a
|
944
|
+
# Markdown document, obtain approval, and execute the chosen block of code.
|
945
|
+
#
|
946
|
+
# @return [Nil] Returns nil if no code block is selected or an error occurs.
|
947
|
+
def document_inpseq
|
948
|
+
@menu_base_options = @delegate_object
|
949
|
+
@dml_link_state = LinkState.new(
|
950
|
+
block_name: @delegate_object[:block_name],
|
951
|
+
document_filename: @delegate_object[:filename]
|
952
|
+
)
|
953
|
+
@run_state.block_name_from_cli = @dml_link_state.block_name.present?
|
954
|
+
@cli_block_name = @dml_link_state.block_name
|
955
|
+
@dml_now_using_cli = @run_state.block_name_from_cli
|
956
|
+
@dml_menu_default_dname = nil
|
957
|
+
@dml_block_state = SelectedBlockMenuState.new
|
958
|
+
@doc_saved_lines_files = []
|
959
|
+
|
960
|
+
## load file with code lines per options
|
961
|
+
#
|
962
|
+
if @menu_base_options[:load_code].present?
|
963
|
+
@dml_link_state.inherited_lines = []
|
964
|
+
@menu_base_options[:load_code].split(':').map do |path|
|
965
|
+
@dml_link_state.inherited_lines += File.readlines(path, chomp: true)
|
966
|
+
end
|
967
|
+
|
968
|
+
inherited_block_names = []
|
969
|
+
inherited_dependencies = {}
|
970
|
+
selected = { oname: 'load_code' }
|
971
|
+
pop_add_current_code_to_head_and_trigger_load(@dml_link_state, inherited_block_names, code_lines, inherited_dependencies, selected)
|
972
|
+
end
|
973
|
+
|
974
|
+
item_back = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_back_name]))
|
975
|
+
item_edit = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name]))
|
976
|
+
item_load = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name]))
|
977
|
+
item_save = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_save_name]))
|
978
|
+
item_view = format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name]))
|
979
|
+
|
980
|
+
@run_state.batch_random = Random.new.rand
|
981
|
+
@run_state.batch_index = 0
|
982
|
+
|
983
|
+
InputSequencer.new(
|
984
|
+
@delegate_object[:filename],
|
985
|
+
@delegate_object[:input_cli_rest]
|
986
|
+
).run do |msg, data|
|
987
|
+
case msg
|
988
|
+
when :parse_document # once for each menu
|
989
|
+
# puts "@ - parse document #{data}"
|
990
|
+
inpseq_parse_document(data)
|
991
|
+
|
992
|
+
if @delegate_object[:menu_for_saved_lines] && @delegate_object[:document_saved_lines_glob].present?
|
993
|
+
|
994
|
+
sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
|
995
|
+
files = sf ? Dir.glob(sf) : []
|
996
|
+
@doc_saved_lines_files = files.count.positive? ? files : []
|
997
|
+
|
998
|
+
lines_count = @dml_link_state.inherited_lines&.count || 0
|
999
|
+
|
1000
|
+
# add menu items (glob, load, save) and enable selectively
|
1001
|
+
menu_add_disabled_option(sf) if files.count.positive? || lines_count.positive?
|
1002
|
+
menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_load_name])), files.count, 'files', menu_state: MenuState::LOAD)
|
1003
|
+
menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_edit_name])), lines_count, 'lines', menu_state: MenuState::EDIT)
|
1004
|
+
menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_save_name])), lines_count, 'lines', menu_state: MenuState::SAVE)
|
1005
|
+
menu_enable_option(format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[:menu_option_view_name])), lines_count, 'lines', menu_state: MenuState::VIEW)
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
when :display_menu
|
1009
|
+
# warn "@ - display menu:"
|
1010
|
+
# ii_display_menu
|
1011
|
+
@dml_block_state = SelectedBlockMenuState.new
|
1012
|
+
@delegate_object[:block_name] = nil
|
1013
|
+
|
1014
|
+
when :user_choice
|
1015
|
+
if @dml_link_state.block_name.present?
|
1016
|
+
# @prior_block_was_link = true
|
1017
|
+
@dml_block_state.block = @dml_blocks_in_file.find do |item|
|
1018
|
+
item[:oname] == @dml_link_state.block_name
|
1019
|
+
end
|
1020
|
+
@dml_link_state.block_name = nil
|
1021
|
+
else
|
1022
|
+
# puts "? - Select a block to execute (or type #{$texit} to exit):"
|
1023
|
+
break if inpseq_user_choice == :break # into @dml_block_state
|
1024
|
+
break if @dml_block_state.block.nil? # no block matched
|
1025
|
+
end
|
1026
|
+
# puts "! - Executing block: #{data}"
|
1027
|
+
# @dml_block_state.block[:oname]
|
1028
|
+
@dml_block_state.block&.fetch(:oname, nil)
|
1029
|
+
|
1030
|
+
when :execute_block
|
1031
|
+
case (block_name = data)
|
1032
|
+
when item_back
|
1033
|
+
debounce_reset
|
1034
|
+
@menu_user_clicked_back_link = true
|
1035
|
+
load_file_link_state = pop_link_history_and_trigger_load
|
1036
|
+
@dml_link_state = load_file_link_state.link_state
|
1037
|
+
|
1038
|
+
InputSequencer.merge_link_state(
|
1039
|
+
@dml_link_state,
|
1040
|
+
InputSequencer.next_link_state(
|
1041
|
+
block_name: @dml_link_state.block_name,
|
1042
|
+
document_filename: @dml_link_state.document_filename,
|
1043
|
+
prior_block_was_link: true
|
1044
|
+
)
|
1045
|
+
)
|
1046
|
+
|
1047
|
+
when item_edit
|
1048
|
+
edited = edit_text(@dml_link_state.inherited_lines.join("\n"))
|
1049
|
+
@dml_link_state.inherited_lines = edited.split("\n") if edited
|
1050
|
+
InputSequencer.next_link_state(prior_block_was_link: true)
|
1051
|
+
|
1052
|
+
when item_load
|
1053
|
+
debounce_reset
|
1054
|
+
sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
|
1055
|
+
load_filespec = load_filespec_from_expression(sf)
|
1056
|
+
if load_filespec
|
1057
|
+
@dml_link_state.inherited_lines ||= []
|
1058
|
+
@dml_link_state.inherited_lines += File.readlines(load_filespec, chomp: true)
|
1059
|
+
end
|
1060
|
+
InputSequencer.next_link_state(prior_block_was_link: true)
|
1061
|
+
|
1062
|
+
when item_save
|
1063
|
+
debounce_reset
|
1064
|
+
sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
|
1065
|
+
save_filespec = save_filespec_from_expression(sf)
|
1066
|
+
if save_filespec && !write_file_with_directory_creation(
|
1067
|
+
save_filespec,
|
1068
|
+
HashDelegator.join_code_lines(@dml_link_state.inherited_lines)
|
1069
|
+
)
|
1070
|
+
return :break
|
1071
|
+
|
1072
|
+
end
|
1073
|
+
|
1074
|
+
InputSequencer.next_link_state(prior_block_was_link: true)
|
1075
|
+
|
1076
|
+
when item_view
|
1077
|
+
warn @dml_link_state.inherited_lines.join("\n")
|
1078
|
+
InputSequencer.next_link_state(prior_block_was_link: true)
|
1079
|
+
|
1080
|
+
else
|
1081
|
+
@dml_block_state = block_state_for_name_from_cli(block_name)
|
1082
|
+
if @dml_block_state.block && @dml_block_state.block.fetch(:shell, nil) == BlockType::OPTS
|
1083
|
+
debounce_reset
|
1084
|
+
link_state = LinkState.new
|
1085
|
+
options_state = read_show_options_and_trigger_reuse(
|
1086
|
+
selected: @dml_block_state.block,
|
1087
|
+
link_state: link_state
|
1088
|
+
)
|
1089
|
+
|
1090
|
+
@menu_base_options.merge!(options_state.options)
|
1091
|
+
@delegate_object.merge!(options_state.options)
|
1092
|
+
options_state.load_file_link_state.link_state
|
1093
|
+
else
|
1094
|
+
inpseq_execute_block(block_name)
|
1095
|
+
|
1096
|
+
if prompt_user_exit(block_name_from_cli: @run_state.block_name_from_cli,
|
1097
|
+
selected: @dml_block_state.block)
|
1098
|
+
return :break
|
1099
|
+
end
|
1100
|
+
|
1101
|
+
## order of block name processing: link block, cli, from user
|
1102
|
+
#
|
1103
|
+
@dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \
|
1104
|
+
HashDelegator.next_link_state(
|
1105
|
+
block_name: @dml_link_state.block_name,
|
1106
|
+
block_name_from_cli: !@dml_link_state.block_name.present?,
|
1107
|
+
block_state: @dml_block_state,
|
1108
|
+
was_using_cli: @dml_now_using_cli
|
1109
|
+
)
|
1110
|
+
|
1111
|
+
if !@dml_block_state.block[:block_name_from_ui] && cli_break
|
1112
|
+
# &bsp '!block_name_from_ui + cli_break -> break'
|
1113
|
+
return :break
|
1114
|
+
end
|
1115
|
+
|
1116
|
+
InputSequencer.next_link_state(
|
1117
|
+
block_name: @dml_link_state.block_name,
|
1118
|
+
prior_block_was_link: @dml_block_state.block.fetch(:shell, nil) != BlockType::BASH
|
1119
|
+
)
|
1120
|
+
end
|
1121
|
+
end
|
1122
|
+
|
1123
|
+
when :exit?
|
1124
|
+
data == $texit
|
1125
|
+
when :stay?
|
1126
|
+
data == $stay
|
1127
|
+
else
|
1128
|
+
raise "Invalid message: #{msg}"
|
1129
|
+
end
|
1130
|
+
end
|
1131
|
+
rescue StandardError
|
1132
|
+
HashDelegator.error_handler('document_inpseq',
|
1133
|
+
{ abort: true })
|
1134
|
+
end
|
1135
|
+
|
1136
|
+
# remove leading "./"
|
1137
|
+
# replace characters: / : . * (space) with: (underscore)
|
1138
|
+
def document_name_in_glob_as_file_name(document_filename, glob)
|
1139
|
+
return document_filename if document_filename.nil? || document_filename.empty?
|
1140
|
+
|
1141
|
+
format(glob, { document_filename: document_filename.gsub(%r{^\./}, '').gsub(/[\/:\.\* ]/, '_') })
|
1142
|
+
end
|
1143
|
+
|
1144
|
+
def dump_and_warn_block_state(selected:)
|
1145
|
+
if selected.nil?
|
1146
|
+
Exceptions.warn_format("Block not found -- name: #{@delegate_object[:block_name]}",
|
1147
|
+
{ abort: true })
|
1148
|
+
end
|
1149
|
+
|
1150
|
+
return unless @delegate_object[:dump_selected_block]
|
1151
|
+
|
1152
|
+
warn selected.to_yaml.sub(/^(?:---\n)?/, "Block:\n")
|
1153
|
+
end
|
1154
|
+
|
1155
|
+
# Outputs warnings based on the delegate object's configuration
|
1156
|
+
#
|
1157
|
+
# @param delegate_object [Hash] The delegate object containing configuration flags.
|
1158
|
+
# @param blocks_in_file [Hash] Hash of blocks present in the file.
|
1159
|
+
# @param menu_blocks [Hash] Hash of menu blocks.
|
1160
|
+
# @param link_state [LinkState] Current state of the link.
|
1161
|
+
def dump_delobj(blocks_in_file, menu_blocks, link_state)
|
1162
|
+
warn format_and_highlight_hash(@delegate_object, label: '@delegate_object') if @delegate_object[:dump_delegate_object]
|
1163
|
+
|
1164
|
+
if @delegate_object[:dump_blocks_in_file]
|
1165
|
+
warn format_and_highlight_dependencies(compact_and_index_hash(blocks_in_file),
|
1166
|
+
label: 'blocks_in_file')
|
1167
|
+
end
|
1168
|
+
|
1169
|
+
if @delegate_object[:dump_menu_blocks]
|
1170
|
+
warn format_and_highlight_dependencies(compact_and_index_hash(menu_blocks),
|
1171
|
+
label: 'menu_blocks')
|
1172
|
+
end
|
1173
|
+
|
1174
|
+
warn format_and_highlight_lines(link_state.inherited_block_names, label: 'inherited_block_names') if @delegate_object[:dump_inherited_block_names]
|
1175
|
+
warn format_and_highlight_lines(link_state.inherited_dependencies, label: 'inherited_dependencies') if @delegate_object[:dump_inherited_dependencies]
|
1176
|
+
return unless @delegate_object[:dump_inherited_lines]
|
1177
|
+
|
1178
|
+
warn format_and_highlight_lines(link_state.inherited_lines, label: 'inherited_lines')
|
1179
|
+
end
|
1180
|
+
|
1181
|
+
# Opens text in an editor for user modification and returns the modified text.
|
1182
|
+
#
|
1183
|
+
# This method reads the provided text, opens it in the default editor,
|
1184
|
+
# and allows the user to modify it. If the user makes changes, the
|
1185
|
+
# modified text is returned. If the user exits the editor without
|
1186
|
+
# making changes or the editor is closed abruptly, appropriate messages
|
1187
|
+
# are displayed.
|
1188
|
+
#
|
1189
|
+
# @param [String] initial_text The initial text to be edited.
|
1190
|
+
# @param [String] temp_name The base name for the temporary file (default: 'edit_text').
|
1191
|
+
# @return [String, nil] The modified text, or nil if no changes were made or the editor was closed abruptly.
|
1192
|
+
def edit_text(initial_text, temp_name: 'edit_text')
|
1193
|
+
# Create a temporary file to store the initial text
|
1194
|
+
temp_file = Tempfile.new(temp_name)
|
1195
|
+
temp_file.write(initial_text)
|
1196
|
+
temp_file.rewind
|
1197
|
+
|
1198
|
+
# Capture the modification time of the temporary file before editing
|
1199
|
+
before_mtime = temp_file.mtime
|
1200
|
+
|
1201
|
+
# Open the temporary file in the default editor
|
1202
|
+
system("#{ENV['EDITOR'] || 'vi'} #{temp_file.path}")
|
1203
|
+
|
1204
|
+
# Capture the exit status of the editor
|
1205
|
+
editor_exit_status = $?.exitstatus
|
1206
|
+
|
1207
|
+
# Reopen the file to ensure the updated modification time is read
|
1208
|
+
temp_file.open
|
1209
|
+
after_mtime = temp_file.mtime
|
1210
|
+
|
1211
|
+
# Check if the editor was exited normally or was interrupted
|
1212
|
+
if editor_exit_status != 0
|
1213
|
+
warn 'The editor was closed abruptly. No changes were made.'
|
1214
|
+
temp_file.close
|
1215
|
+
temp_file.unlink
|
1216
|
+
return
|
1217
|
+
end
|
1218
|
+
|
1219
|
+
result_text = nil
|
1220
|
+
# Read the file if it was modified
|
1221
|
+
if before_mtime != after_mtime
|
1222
|
+
temp_file.rewind
|
1223
|
+
result_text = temp_file.read
|
1224
|
+
end
|
1225
|
+
|
1226
|
+
# Remove the temporary file
|
1227
|
+
temp_file.close
|
1228
|
+
temp_file.unlink
|
1229
|
+
|
1230
|
+
result_text
|
1231
|
+
end
|
1232
|
+
|
1233
|
+
def exec_bash_next_state(selected:, mdoc:, link_state:, block_source: {})
|
1234
|
+
lfls = execute_shell_type(
|
1235
|
+
selected: selected,
|
1236
|
+
mdoc: mdoc,
|
1237
|
+
link_state: link_state,
|
1238
|
+
block_source: block_source
|
1239
|
+
)
|
1240
|
+
|
1241
|
+
# if the same menu is being displayed, collect the display name of the selected menu item for use as the default item
|
1242
|
+
[lfls.link_state,
|
1243
|
+
lfls.load_file == LoadFile::LOAD ? nil : selected[:dname]]
|
1244
|
+
#.tap { |ret| pp [__FILE__,__LINE__,'exec_bash_next_state()',ret] }
|
1245
|
+
end
|
1246
|
+
|
953
1247
|
# Executes a block of code that has been approved for execution.
|
954
1248
|
# It sets the script block name, writes command files if required, and handles the execution
|
955
1249
|
# including output formatting and summarization.
|
@@ -1007,7 +1301,7 @@ module MarkdownExec
|
|
1007
1301
|
inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
|
1008
1302
|
next_block_name: '',
|
1009
1303
|
next_document_filename: @delegate_object[:filename],
|
1010
|
-
next_load_file: LoadFile::
|
1304
|
+
next_load_file: LoadFile::REUSE
|
1011
1305
|
)
|
1012
1306
|
|
1013
1307
|
elsif selected[:shell] == BlockType::VARS
|
@@ -1023,7 +1317,7 @@ module MarkdownExec
|
|
1023
1317
|
inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
|
1024
1318
|
next_block_name: '',
|
1025
1319
|
next_document_filename: @delegate_object[:filename],
|
1026
|
-
next_load_file: LoadFile::
|
1320
|
+
next_load_file: LoadFile::REUSE
|
1027
1321
|
)
|
1028
1322
|
|
1029
1323
|
elsif debounce_allows
|
@@ -1033,7 +1327,7 @@ module MarkdownExec
|
|
1033
1327
|
block_source: block_source)
|
1034
1328
|
|
1035
1329
|
else
|
1036
|
-
LoadFileLinkState.new(LoadFile::
|
1330
|
+
LoadFileLinkState.new(LoadFile::REUSE, link_state)
|
1037
1331
|
end
|
1038
1332
|
end
|
1039
1333
|
|
@@ -1060,6 +1354,23 @@ module MarkdownExec
|
|
1060
1354
|
color_sym: :script_execution_frame_color)
|
1061
1355
|
end
|
1062
1356
|
|
1357
|
+
# Format expression using environment variables and run state
|
1358
|
+
def format_expression(expr)
|
1359
|
+
data = link_load_format_data
|
1360
|
+
ENV.each { |key, value| data[key] = value }
|
1361
|
+
format(expr, data)
|
1362
|
+
end
|
1363
|
+
|
1364
|
+
# Formats multiline body content as a title string.
|
1365
|
+
# indents all but first line with two spaces so it displays correctly in menu
|
1366
|
+
# @param body_lines [Array<String>] The lines of body content.
|
1367
|
+
# @return [String] Formatted title.
|
1368
|
+
def format_multiline_body_as_title(body_lines)
|
1369
|
+
body_lines.map.with_index do |line, index|
|
1370
|
+
index.zero? ? line : " #{line}"
|
1371
|
+
end.join("\n") + "\n"
|
1372
|
+
end
|
1373
|
+
|
1063
1374
|
# Formats a string based on a given context and applies color styling to it.
|
1064
1375
|
# It retrieves format and color information from the delegate object and processes accordingly.
|
1065
1376
|
#
|
@@ -1076,9 +1387,14 @@ module MarkdownExec
|
|
1076
1387
|
string_send_color(formatted_string, color_sym)
|
1077
1388
|
end
|
1078
1389
|
|
1079
|
-
#
|
1080
|
-
|
1081
|
-
|
1390
|
+
# Expand expression if it contains format specifiers
|
1391
|
+
def formatted_expression(expr)
|
1392
|
+
expr.include?('%{') ? format_expression(expr) : expr
|
1393
|
+
end
|
1394
|
+
|
1395
|
+
# Processes a block to generate its summary, modifying its attributes based on various matching criteria.
|
1396
|
+
# It handles special formatting for bash blocks, extracting and setting properties like call, stdin, stdout, and dname.
|
1397
|
+
#
|
1082
1398
|
# @param fcb [Object] An object representing a functional code block.
|
1083
1399
|
# @return [Object] The modified functional code block with updated summary attributes.
|
1084
1400
|
def get_block_summary(fcb)
|
@@ -1110,16 +1426,6 @@ module MarkdownExec
|
|
1110
1426
|
fcb
|
1111
1427
|
end
|
1112
1428
|
|
1113
|
-
# Formats multiline body content as a title string.
|
1114
|
-
# indents all but first line with two spaces so it displays correctly in menu
|
1115
|
-
# @param body_lines [Array<String>] The lines of body content.
|
1116
|
-
# @return [String] Formatted title.
|
1117
|
-
def format_multiline_body_as_title(body_lines)
|
1118
|
-
body_lines.map.with_index do |line, index|
|
1119
|
-
index.zero? ? line : " #{line}"
|
1120
|
-
end.join("\n") + "\n"
|
1121
|
-
end
|
1122
|
-
|
1123
1429
|
# Updates the delegate object's state based on the provided block state.
|
1124
1430
|
# It sets the block name and determines if the user clicked the back link in the menu.
|
1125
1431
|
#
|
@@ -1172,6 +1478,46 @@ module MarkdownExec
|
|
1172
1478
|
}
|
1173
1479
|
end
|
1174
1480
|
|
1481
|
+
def inpseq_execute_block(block_name)
|
1482
|
+
@dml_block_state = block_state_for_name_from_cli(block_name)
|
1483
|
+
|
1484
|
+
dump_and_warn_block_state(selected: @dml_block_state.block)
|
1485
|
+
@dml_link_state, @dml_menu_default_dname = \
|
1486
|
+
exec_bash_next_state(
|
1487
|
+
selected: @dml_block_state.block,
|
1488
|
+
mdoc: @dml_mdoc,
|
1489
|
+
link_state: @dml_link_state,
|
1490
|
+
block_source: {
|
1491
|
+
document_filename: @delegate_object[:filename],
|
1492
|
+
time_now_date: Time.now.utc.strftime(@delegate_object[:shell_code_label_time_format])
|
1493
|
+
}
|
1494
|
+
)
|
1495
|
+
end
|
1496
|
+
|
1497
|
+
def inpseq_parse_document(_document_filename)
|
1498
|
+
@run_state.batch_index += 1
|
1499
|
+
@run_state.in_own_window = false
|
1500
|
+
|
1501
|
+
# &bsp 'loop', block_name_from_cli, @cli_block_name
|
1502
|
+
@run_state.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc = \
|
1503
|
+
set_delobj_menu_loop_vars(block_name_from_cli: @run_state.block_name_from_cli,
|
1504
|
+
now_using_cli: @dml_now_using_cli,
|
1505
|
+
link_state: @dml_link_state)
|
1506
|
+
end
|
1507
|
+
|
1508
|
+
def inpseq_user_choice
|
1509
|
+
@dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file,
|
1510
|
+
menu_blocks: @dml_menu_blocks,
|
1511
|
+
default: @dml_menu_default_dname)
|
1512
|
+
# &bsp '@run_state.block_name_from_cli:',@run_state.block_name_from_cli
|
1513
|
+
if !@dml_block_state
|
1514
|
+
HashDelegator.error_handler('block_state missing', { abort: true })
|
1515
|
+
elsif @dml_block_state.state == MenuState::EXIT
|
1516
|
+
# &bsp 'load_cli_or_user_selected_block -> break'
|
1517
|
+
:break
|
1518
|
+
end
|
1519
|
+
end
|
1520
|
+
|
1175
1521
|
# Iterates through blocks in a file, applying the provided block to each line.
|
1176
1522
|
# The iteration only occurs if the file exists.
|
1177
1523
|
# @yield [Symbol] :filter Yields to obtain selected messages for processing.
|
@@ -1198,18 +1544,18 @@ module MarkdownExec
|
|
1198
1544
|
file.write(all_code.join("\n"))
|
1199
1545
|
file.rewind
|
1200
1546
|
|
1201
|
-
if link_block_data.fetch(LinkKeys::
|
1547
|
+
if link_block_data.fetch(LinkKeys::EXEC, false)
|
1202
1548
|
@run_state.files = Hash.new([])
|
1203
1549
|
|
1204
1550
|
Open3.popen3(cmd) do |stdin, stdout, stderr, _exec_thr|
|
1205
|
-
handle_stream(stream: stdout, file_type: ExecutionStreams::
|
1551
|
+
handle_stream(stream: stdout, file_type: ExecutionStreams::STD_OUT) do |line|
|
1206
1552
|
output_lines.push(line)
|
1207
1553
|
end
|
1208
|
-
handle_stream(stream: stderr, file_type: ExecutionStreams::
|
1554
|
+
handle_stream(stream: stderr, file_type: ExecutionStreams::STD_ERR) do |line|
|
1209
1555
|
output_lines.push(line)
|
1210
1556
|
end
|
1211
1557
|
|
1212
|
-
in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::
|
1558
|
+
in_thr = handle_stream(stream: $stdin, file_type: ExecutionStreams::STD_IN) do |line|
|
1213
1559
|
stdin.puts(line)
|
1214
1560
|
end
|
1215
1561
|
|
@@ -1275,114 +1621,6 @@ module MarkdownExec
|
|
1275
1621
|
)
|
1276
1622
|
end
|
1277
1623
|
|
1278
|
-
# format + glob + select for file in load block
|
1279
|
-
# name has references to ENV vars and doc and batch vars incl. timestamp
|
1280
|
-
def load_filespec_from_expression(expression)
|
1281
|
-
# Process expression with embedded formatting
|
1282
|
-
expanded_expression = formatted_expression(expression)
|
1283
|
-
|
1284
|
-
# Handle wildcards or direct file specification
|
1285
|
-
if contains_wildcards?(expanded_expression)
|
1286
|
-
load_filespec_wildcard_expansion(expanded_expression)
|
1287
|
-
else
|
1288
|
-
expanded_expression
|
1289
|
-
end
|
1290
|
-
end
|
1291
|
-
|
1292
|
-
def save_filespec_from_expression(expression)
|
1293
|
-
# Process expression with embedded formatting
|
1294
|
-
formatted = formatted_expression(expression)
|
1295
|
-
|
1296
|
-
# Handle wildcards or direct file specification
|
1297
|
-
if contains_wildcards?(formatted)
|
1298
|
-
save_filespec_wildcard_expansion(formatted)
|
1299
|
-
else
|
1300
|
-
formatted
|
1301
|
-
end
|
1302
|
-
end
|
1303
|
-
|
1304
|
-
# private
|
1305
|
-
|
1306
|
-
# Expand expression if it contains format specifiers
|
1307
|
-
def formatted_expression(expr)
|
1308
|
-
expr.include?('%{') ? format_expression(expr) : expr
|
1309
|
-
end
|
1310
|
-
|
1311
|
-
# Format expression using environment variables and run state
|
1312
|
-
def format_expression(expr)
|
1313
|
-
data = link_load_format_data
|
1314
|
-
ENV.each { |key, value| data[key] = value }
|
1315
|
-
format(expr, data)
|
1316
|
-
end
|
1317
|
-
|
1318
|
-
# Check if the expression contains wildcard characters
|
1319
|
-
def contains_wildcards?(expr)
|
1320
|
-
expr.match(%r{\*|\?|\[})
|
1321
|
-
end
|
1322
|
-
|
1323
|
-
# Handle expression with wildcard characters
|
1324
|
-
def load_filespec_wildcard_expansion(expr)
|
1325
|
-
files = find_files(expr)
|
1326
|
-
case files.count
|
1327
|
-
when 0
|
1328
|
-
HashDelegator.error_handler("no files found with '#{expr}' ", { abort: true })
|
1329
|
-
when 1
|
1330
|
-
files.first
|
1331
|
-
else
|
1332
|
-
prompt_select_code_filename(files)
|
1333
|
-
end
|
1334
|
-
end
|
1335
|
-
|
1336
|
-
# Handle expression with wildcard characters
|
1337
|
-
# allow user to select or enter
|
1338
|
-
def puts_gets_oprompt_(filespec)
|
1339
|
-
puts format(@delegate_object[:prompt_show_expr_format],
|
1340
|
-
{ expr: filespec })
|
1341
|
-
puts @delegate_object[:prompt_enter_filespec]
|
1342
|
-
gets.chomp
|
1343
|
-
end
|
1344
|
-
|
1345
|
-
# prompt user to enter a path (i.e. containing a path separator)
|
1346
|
-
# or name to substitute into the wildcard expression
|
1347
|
-
def prompt_for_filespec_with_wildcard(filespec)
|
1348
|
-
puts format(@delegate_object[:prompt_show_expr_format],
|
1349
|
-
{ expr: filespec })
|
1350
|
-
puts @delegate_object[:prompt_enter_filespec]
|
1351
|
-
PathUtils.resolve_path_or_substitute(gets.chomp, filespec)
|
1352
|
-
end
|
1353
|
-
|
1354
|
-
# def read_block_name(line)
|
1355
|
-
# bm = extract_named_captures_from_option(line, @delegate_object[:block_name_match])
|
1356
|
-
# name = bm[:title]
|
1357
|
-
|
1358
|
-
# if @delegate_object[:block_name_nick_match].present? && line =~ Regexp.new(@delegate_object[:block_name_nick_match])
|
1359
|
-
# name = $~[0]
|
1360
|
-
# else
|
1361
|
-
# name = bm && bm[1] ? bm[:title] : name
|
1362
|
-
# end
|
1363
|
-
# name
|
1364
|
-
# end
|
1365
|
-
|
1366
|
-
# Handle expression with wildcard characters
|
1367
|
-
# allow user to select or enter
|
1368
|
-
def save_filespec_wildcard_expansion(filespec)
|
1369
|
-
files = find_files(filespec)
|
1370
|
-
case files.count
|
1371
|
-
when 0
|
1372
|
-
prompt_for_filespec_with_wildcard(filespec)
|
1373
|
-
else
|
1374
|
-
## user selects from existing files or other
|
1375
|
-
# input into path with wildcard for easy entry
|
1376
|
-
#
|
1377
|
-
name = prompt_select_code_filename([@delegate_object[:prompt_filespec_other]] + files)
|
1378
|
-
if name == @delegate_object[:prompt_filespec_other]
|
1379
|
-
prompt_for_filespec_with_wildcard(filespec)
|
1380
|
-
else
|
1381
|
-
name
|
1382
|
-
end
|
1383
|
-
end
|
1384
|
-
end
|
1385
|
-
|
1386
1624
|
def link_load_format_data
|
1387
1625
|
{
|
1388
1626
|
batch_index: @run_state.batch_index,
|
@@ -1395,29 +1633,6 @@ module MarkdownExec
|
|
1395
1633
|
}
|
1396
1634
|
end
|
1397
1635
|
|
1398
|
-
# # Loads auto link block.
|
1399
|
-
# def load_auto_link_block(all_blocks, link_state, mdoc, block_source:)
|
1400
|
-
# block_name = @delegate_object[:document_load_link_block_name]
|
1401
|
-
# return unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename]
|
1402
|
-
|
1403
|
-
# block = HashDelegator.block_find(all_blocks, :oname, block_name)
|
1404
|
-
# return unless block
|
1405
|
-
|
1406
|
-
# if block.fetch(:shell, '') != BlockType::LINK
|
1407
|
-
# HashDelegator.error_handler('must be Link block type', { abort: true })
|
1408
|
-
|
1409
|
-
# else
|
1410
|
-
# # debounce_reset
|
1411
|
-
# push_link_history_and_trigger_load(
|
1412
|
-
# link_block_body: block.fetch(:body, ''),
|
1413
|
-
# mdoc: mdoc,
|
1414
|
-
# selected: block,
|
1415
|
-
# link_state: link_state,
|
1416
|
-
# block_source: block_source
|
1417
|
-
# )
|
1418
|
-
# end
|
1419
|
-
# end
|
1420
|
-
|
1421
1636
|
# Loads auto blocks based on delegate object settings and updates if new filename is detected.
|
1422
1637
|
# Executes a specified block once per filename.
|
1423
1638
|
# @param all_blocks [Array] Array of all block elements.
|
@@ -1437,6 +1652,55 @@ module MarkdownExec
|
|
1437
1652
|
true
|
1438
1653
|
end
|
1439
1654
|
|
1655
|
+
def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil)
|
1656
|
+
if @delegate_object[:block_name].present?
|
1657
|
+
block = all_blocks.find do |item|
|
1658
|
+
item[:oname] == @delegate_object[:block_name]
|
1659
|
+
end&.merge(block_name_from_ui: false)
|
1660
|
+
else
|
1661
|
+
block_state = wait_for_user_selected_block(all_blocks, menu_blocks,
|
1662
|
+
default)
|
1663
|
+
block = block_state.block&.merge(block_name_from_ui: true)
|
1664
|
+
state = block_state.state
|
1665
|
+
end
|
1666
|
+
|
1667
|
+
SelectedBlockMenuState.new(block, state)
|
1668
|
+
rescue StandardError
|
1669
|
+
HashDelegator.error_handler('load_cli_or_user_selected_block')
|
1670
|
+
end
|
1671
|
+
|
1672
|
+
# format + glob + select for file in load block
|
1673
|
+
# name has references to ENV vars and doc and batch vars incl. timestamp
|
1674
|
+
def load_filespec_from_expression(expression)
|
1675
|
+
# Process expression with embedded formatting
|
1676
|
+
expanded_expression = formatted_expression(expression)
|
1677
|
+
|
1678
|
+
# Handle wildcards or direct file specification
|
1679
|
+
if contains_wildcards?(expanded_expression)
|
1680
|
+
load_filespec_wildcard_expansion(expanded_expression)
|
1681
|
+
else
|
1682
|
+
expanded_expression
|
1683
|
+
end
|
1684
|
+
end
|
1685
|
+
# Handle expression with wildcard characters
|
1686
|
+
def load_filespec_wildcard_expansion(expr, auto_load_single: false)
|
1687
|
+
files = find_files(expr)
|
1688
|
+
if files.count.zero?
|
1689
|
+
HashDelegator.error_handler("no files found with '#{expr}' ", { abort: true })
|
1690
|
+
elsif auto_load_single && files.count == 1
|
1691
|
+
files.first
|
1692
|
+
else
|
1693
|
+
## user selects from existing files or other
|
1694
|
+
#
|
1695
|
+
case (name = prompt_select_code_filename([@delegate_object[:prompt_filespec_back]] + files))
|
1696
|
+
when @delegate_object[:prompt_filespec_back]
|
1697
|
+
# do nothing
|
1698
|
+
else
|
1699
|
+
name
|
1700
|
+
end
|
1701
|
+
end
|
1702
|
+
end
|
1703
|
+
|
1440
1704
|
def mdoc_and_blocks_from_nested_files
|
1441
1705
|
menu_blocks = blocks_from_nested_files
|
1442
1706
|
mdoc = MDoc.new(menu_blocks) do |nopts|
|
@@ -1453,15 +1717,41 @@ module MarkdownExec
|
|
1453
1717
|
# recreate menu with new options
|
1454
1718
|
#
|
1455
1719
|
all_blocks, mdoc = mdoc_and_blocks_from_nested_files if load_auto_opts_block(all_blocks)
|
1456
|
-
# load_auto_link_block(all_blocks, link_state, mdoc, block_source: {})
|
1457
1720
|
|
1458
1721
|
menu_blocks = mdoc.fcbs_per_options(@delegate_object)
|
1459
1722
|
add_menu_chrome_blocks!(menu_blocks: menu_blocks, link_state: link_state)
|
1460
1723
|
### compress empty lines
|
1461
|
-
HashDelegator.delete_consecutive_blank_lines!(menu_blocks)
|
1724
|
+
HashDelegator.delete_consecutive_blank_lines!(menu_blocks)
|
1462
1725
|
[all_blocks, menu_blocks, mdoc]
|
1463
1726
|
end
|
1464
1727
|
|
1728
|
+
def menu_add_disabled_option(name)
|
1729
|
+
raise unless name.present?
|
1730
|
+
raise if @dml_menu_blocks.nil?
|
1731
|
+
|
1732
|
+
block = @dml_menu_blocks.find { |item| item[:oname] == name }
|
1733
|
+
|
1734
|
+
# create menu item when it is needed (count > 0)
|
1735
|
+
#
|
1736
|
+
return unless block.nil?
|
1737
|
+
|
1738
|
+
# append_chrome_block(menu_blocks: @dml_menu_blocks, menu_state: MenuState::LOAD)
|
1739
|
+
chrome_block = FCB.new(
|
1740
|
+
chrome: true,
|
1741
|
+
disabled: '',
|
1742
|
+
dname: HashDelegator.new(@delegate_object).string_send_color(
|
1743
|
+
name, :menu_inherited_lines_color
|
1744
|
+
),
|
1745
|
+
oname: formatted_name
|
1746
|
+
)
|
1747
|
+
|
1748
|
+
if insert_at_top
|
1749
|
+
@dml_menu_blocks.unshift(chrome_block)
|
1750
|
+
else
|
1751
|
+
@dml_menu_blocks.push(chrome_block)
|
1752
|
+
end
|
1753
|
+
end
|
1754
|
+
|
1465
1755
|
# Formats and optionally colors a menu option based on delegate object's configuration.
|
1466
1756
|
# @param option_symbol [Symbol] The symbol key for the menu option in the delegate object.
|
1467
1757
|
# @return [String] The formatted and possibly colored value of the menu option.
|
@@ -1486,6 +1776,47 @@ module MarkdownExec
|
|
1486
1776
|
end
|
1487
1777
|
end
|
1488
1778
|
|
1779
|
+
def menu_enable_option(name, count, type, menu_state: MenuState::LOAD)
|
1780
|
+
raise unless name.present?
|
1781
|
+
raise if @dml_menu_blocks.nil?
|
1782
|
+
|
1783
|
+
item = @dml_menu_blocks.find { |block| block[:oname] == name }
|
1784
|
+
|
1785
|
+
# create menu item when it is needed (count > 0)
|
1786
|
+
#
|
1787
|
+
if item.nil? && count.positive?
|
1788
|
+
append_chrome_block(menu_blocks: @dml_menu_blocks, menu_state: menu_state)
|
1789
|
+
item = @dml_menu_blocks.find { |block| block[:oname] == name }
|
1790
|
+
end
|
1791
|
+
|
1792
|
+
# update item if it exists
|
1793
|
+
#
|
1794
|
+
return unless item
|
1795
|
+
|
1796
|
+
item[:dname] = "#{name} (#{count} #{type})"
|
1797
|
+
if count.positive?
|
1798
|
+
item.delete(:disabled)
|
1799
|
+
else
|
1800
|
+
item[:disabled] = ''
|
1801
|
+
end
|
1802
|
+
end
|
1803
|
+
|
1804
|
+
def manage_cli_selection_state(block_name_from_cli:, now_using_cli:, link_state:)
|
1805
|
+
if block_name_from_cli && @cli_block_name == @menu_base_options[:menu_persist_block_name]
|
1806
|
+
# &bsp 'pause cli control, allow user to select block'
|
1807
|
+
block_name_from_cli = false
|
1808
|
+
now_using_cli = false
|
1809
|
+
@menu_base_options[:block_name] = \
|
1810
|
+
@delegate_object[:block_name] = \
|
1811
|
+
link_state.block_name = \
|
1812
|
+
@cli_block_name = nil
|
1813
|
+
end
|
1814
|
+
|
1815
|
+
@delegate_object = @menu_base_options.dup
|
1816
|
+
@menu_user_clicked_back_link = false
|
1817
|
+
[block_name_from_cli, now_using_cli]
|
1818
|
+
end
|
1819
|
+
|
1489
1820
|
# If a method is missing, treat it as a key for the @delegate_object.
|
1490
1821
|
def method_missing(method_name, *args, &block)
|
1491
1822
|
if @delegate_object.respond_to?(method_name)
|
@@ -1567,7 +1898,7 @@ module MarkdownExec
|
|
1567
1898
|
@link_history.push(next_state)
|
1568
1899
|
|
1569
1900
|
next_state.block_name = nil
|
1570
|
-
LoadFileLinkState.new(LoadFile::
|
1901
|
+
LoadFileLinkState.new(LoadFile::LOAD, next_state)
|
1571
1902
|
else
|
1572
1903
|
# no history exists; must have been called independently => retain script
|
1573
1904
|
link_history_push_and_next(
|
@@ -1576,11 +1907,11 @@ module MarkdownExec
|
|
1576
1907
|
inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
|
1577
1908
|
inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
|
1578
1909
|
inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
|
1579
|
-
next_block_name: '', # not link_block_data[LinkKeys::
|
1910
|
+
next_block_name: '', # not link_block_data[LinkKeys::BLOCK] || ''
|
1580
1911
|
next_document_filename: @delegate_object[:filename], # not next_document_filename
|
1581
|
-
next_load_file: LoadFile::
|
1912
|
+
next_load_file: LoadFile::REUSE # not next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD
|
1582
1913
|
)
|
1583
|
-
# LoadFileLinkState.new(LoadFile::
|
1914
|
+
# LoadFileLinkState.new(LoadFile::REUSE, link_state)
|
1584
1915
|
end
|
1585
1916
|
end
|
1586
1917
|
|
@@ -1591,7 +1922,7 @@ module MarkdownExec
|
|
1591
1922
|
def pop_link_history_and_trigger_load
|
1592
1923
|
pop = @link_history.pop
|
1593
1924
|
peek = @link_history.peek
|
1594
|
-
LoadFileLinkState.new(LoadFile::
|
1925
|
+
LoadFileLinkState.new(LoadFile::LOAD, LinkState.new(
|
1595
1926
|
document_filename: pop.document_filename,
|
1596
1927
|
inherited_block_names: peek.inherited_block_names,
|
1597
1928
|
inherited_dependencies: peek.inherited_dependencies,
|
@@ -1705,6 +2036,24 @@ module MarkdownExec
|
|
1705
2036
|
exit 1
|
1706
2037
|
end
|
1707
2038
|
|
2039
|
+
# Prompts the user to enter a path or name to substitute into the wildcard expression.
|
2040
|
+
# If interrupted by the user (e.g., pressing Ctrl-C), it returns nil.
|
2041
|
+
#
|
2042
|
+
# @param filespec [String] the wildcard expression to be substituted
|
2043
|
+
# @return [String, nil] the resolved path or substituted expression, or nil if interrupted
|
2044
|
+
def prompt_for_filespec_with_wildcard(filespec)
|
2045
|
+
puts format(@delegate_object[:prompt_show_expr_format], { expr: filespec })
|
2046
|
+
puts @delegate_object[:prompt_enter_filespec]
|
2047
|
+
|
2048
|
+
begin
|
2049
|
+
input = gets.chomp
|
2050
|
+
PathUtils.resolve_path_or_substitute(input, filespec)
|
2051
|
+
rescue Interrupt
|
2052
|
+
puts "\nOperation interrupted. Returning nil."
|
2053
|
+
nil
|
2054
|
+
end
|
2055
|
+
end
|
2056
|
+
|
1708
2057
|
##
|
1709
2058
|
# Presents a menu to the user for approving an action and performs additional tasks based on the selection.
|
1710
2059
|
# The function provides options for approval, rejection, copying data to clipboard, or saving data to a file.
|
@@ -1748,38 +2097,47 @@ module MarkdownExec
|
|
1748
2097
|
exit 1
|
1749
2098
|
end
|
1750
2099
|
|
1751
|
-
|
1752
|
-
|
1753
|
-
|
2100
|
+
# public
|
2101
|
+
|
2102
|
+
def prompt_select_code_filename(filenames)
|
2103
|
+
@prompt.select(
|
2104
|
+
string_send_color(@delegate_object[:prompt_select_code_file],
|
1754
2105
|
:prompt_color_after_script_execution),
|
1755
2106
|
filter: true,
|
1756
2107
|
quiet: true
|
1757
2108
|
) do |menu|
|
1758
|
-
|
1759
|
-
|
1760
|
-
|
1761
|
-
|
2109
|
+
filenames.each do |filename|
|
2110
|
+
menu.choice filename
|
2111
|
+
end
|
2112
|
+
end
|
1762
2113
|
rescue TTY::Reader::InputInterrupt
|
1763
2114
|
exit 1
|
1764
2115
|
end
|
1765
2116
|
|
1766
|
-
|
1767
|
-
|
1768
|
-
|
1769
|
-
@prompt.select(
|
1770
|
-
string_send_color(@delegate_object[:prompt_select_code_file],
|
2117
|
+
def prompt_select_continue
|
2118
|
+
sel = @prompt.select(
|
2119
|
+
string_send_color(@delegate_object[:prompt_after_script_execution],
|
1771
2120
|
:prompt_color_after_script_execution),
|
1772
2121
|
filter: true,
|
1773
2122
|
quiet: true
|
1774
2123
|
) do |menu|
|
1775
|
-
|
1776
|
-
|
1777
|
-
end
|
2124
|
+
menu.choice @delegate_object[:prompt_yes]
|
2125
|
+
menu.choice @delegate_object[:prompt_exit]
|
1778
2126
|
end
|
2127
|
+
sel == @delegate_object[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
|
1779
2128
|
rescue TTY::Reader::InputInterrupt
|
1780
2129
|
exit 1
|
1781
2130
|
end
|
1782
2131
|
|
2132
|
+
# user prompt to exit if the menu will be displayed again
|
2133
|
+
#
|
2134
|
+
def prompt_user_exit(block_name_from_cli:, selected:)
|
2135
|
+
!block_name_from_cli &&
|
2136
|
+
selected[:shell] == BlockType::BASH &&
|
2137
|
+
@delegate_object[:pause_after_script_execution] &&
|
2138
|
+
prompt_select_continue == MenuState::EXIT
|
2139
|
+
end
|
2140
|
+
|
1783
2141
|
# Handles the processing of a link block in Markdown Execution.
|
1784
2142
|
# It loads YAML data from the link_block_body content, pushes the state to history,
|
1785
2143
|
# sets environment variables, and decides on the next block to load.
|
@@ -1812,368 +2170,153 @@ module MarkdownExec
|
|
1812
2170
|
|
1813
2171
|
# load key and values from link block into current environment
|
1814
2172
|
#
|
1815
|
-
if link_block_data[LinkKeys::
|
2173
|
+
if link_block_data[LinkKeys::VARS]
|
1816
2174
|
code_lines.push BashCommentFormatter.format_comment(selected[:oname])
|
1817
|
-
(link_block_data[LinkKeys::
|
1818
|
-
ENV[key] = value.to_s
|
1819
|
-
code_lines.push(assign_key_value_in_bash(key, value))
|
1820
|
-
end
|
1821
|
-
end
|
1822
|
-
|
1823
|
-
## append blocks loaded, apply LinkKeys::Eval
|
1824
|
-
#
|
1825
|
-
if (load_expr = link_block_data.fetch(LinkKeys::Load, '')).present?
|
1826
|
-
load_filespec = load_filespec_from_expression(load_expr)
|
1827
|
-
code_lines += File.readlines(load_filespec, chomp: true) if load_filespec
|
1828
|
-
end
|
1829
|
-
|
1830
|
-
# if an eval link block, evaluate code_lines and return its standard output
|
1831
|
-
#
|
1832
|
-
if link_block_data.fetch(LinkKeys::Eval,
|
1833
|
-
false) || link_block_data.fetch(LinkKeys::Exec, false)
|
1834
|
-
code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source: block_source)
|
1835
|
-
end
|
1836
|
-
|
1837
|
-
next_document_filename = write_inherited_lines_to_file(link_state, link_block_data)
|
1838
|
-
|
1839
|
-
if link_block_data[LinkKeys::Return]
|
1840
|
-
pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines,
|
1841
|
-
dependencies, selected)
|
1842
|
-
|
1843
|
-
else
|
1844
|
-
link_history_push_and_next(
|
1845
|
-
curr_block_name: selected[:oname],
|
1846
|
-
curr_document_filename: @delegate_object[:filename],
|
1847
|
-
inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
|
1848
|
-
inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
|
1849
|
-
inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
|
1850
|
-
next_block_name: link_block_data.fetch(LinkKeys::NextBlock,
|
1851
|
-
nil) || link_block_data[LinkKeys::Block] || '',
|
1852
|
-
next_document_filename: next_document_filename,
|
1853
|
-
next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::Reuse : LoadFile::Load
|
1854
|
-
)
|
1855
|
-
end
|
1856
|
-
end
|
1857
|
-
|
1858
|
-
# Check if the delegate object responds to a given method.
|
1859
|
-
# @param method_name [Symbol] The name of the method to check.
|
1860
|
-
# @param include_private [Boolean] Whether to include private methods in the check.
|
1861
|
-
# @return [Boolean] true if the delegate object responds to the method, false otherwise.
|
1862
|
-
def respond_to?(method_name, include_private = false)
|
1863
|
-
if super
|
1864
|
-
true
|
1865
|
-
elsif @delegate_object.respond_to?(method_name, include_private)
|
1866
|
-
true
|
1867
|
-
elsif method_name.to_s.end_with?('=') && @delegate_object.respond_to?(:[]=, include_private)
|
1868
|
-
true
|
1869
|
-
else
|
1870
|
-
@delegate_object.respond_to?(method_name, include_private)
|
1871
|
-
end
|
1872
|
-
end
|
1873
|
-
|
1874
|
-
def runtime_exception(exception_sym, name, items)
|
1875
|
-
if @delegate_object[exception_sym] != 0
|
1876
|
-
data = { name: name, detail: items.join(', ') }
|
1877
|
-
warn(
|
1878
|
-
format(
|
1879
|
-
@delegate_object.fetch(:exception_format_name, "\n%{name}"),
|
1880
|
-
data
|
1881
|
-
).send(@delegate_object.fetch(:exception_color_name, :red)) +
|
1882
|
-
format(
|
1883
|
-
@delegate_object.fetch(:exception_format_detail, " - %{detail}\n"),
|
1884
|
-
data
|
1885
|
-
).send(@delegate_object.fetch(:exception_color_detail, :yellow))
|
1886
|
-
)
|
1887
|
-
end
|
1888
|
-
return unless (@delegate_object[exception_sym]).positive?
|
1889
|
-
|
1890
|
-
exit @delegate_object[exception_sym]
|
1891
|
-
end
|
1892
|
-
|
1893
|
-
def save_to_file(required_lines:, selected:)
|
1894
|
-
write_command_file(required_lines: required_lines, selected: selected)
|
1895
|
-
@fout.fout "File saved: #{@run_state.saved_filespec}"
|
1896
|
-
end
|
1897
|
-
|
1898
|
-
def block_state_for_name_from_cli(block_name)
|
1899
|
-
SelectedBlockMenuState.new(
|
1900
|
-
@dml_blocks_in_file.find do |item|
|
1901
|
-
item[:oname] == block_name
|
1902
|
-
end&.merge(
|
1903
|
-
block_name_from_cli: true,
|
1904
|
-
block_name_from_ui: false
|
1905
|
-
),
|
1906
|
-
MenuState::CONTINUE
|
1907
|
-
)
|
1908
|
-
end
|
1909
|
-
|
1910
|
-
# Select and execute a code block from a Markdown document.
|
1911
|
-
#
|
1912
|
-
# This method allows the user to interactively select a code block from a
|
1913
|
-
# Markdown document, obtain approval, and execute the chosen block of code.
|
1914
|
-
#
|
1915
|
-
# @return [Nil] Returns nil if no code block is selected or an error occurs.
|
1916
|
-
def document_menu_loop
|
1917
|
-
@menu_base_options = @delegate_object
|
1918
|
-
@dml_link_state = LinkState.new(
|
1919
|
-
block_name: @delegate_object[:block_name],
|
1920
|
-
document_filename: @delegate_object[:filename]
|
1921
|
-
)
|
1922
|
-
@run_state.block_name_from_cli = @dml_link_state.block_name.present?
|
1923
|
-
@cli_block_name = @dml_link_state.block_name
|
1924
|
-
@dml_now_using_cli = @run_state.block_name_from_cli
|
1925
|
-
@dml_menu_default_dname = nil
|
1926
|
-
@dml_block_state = SelectedBlockMenuState.new
|
1927
|
-
|
1928
|
-
@run_state.batch_random = Random.new.rand
|
1929
|
-
@run_state.batch_index = 0
|
1930
|
-
|
1931
|
-
InputSequencer.new(
|
1932
|
-
@delegate_object[:filename],
|
1933
|
-
@delegate_object[:input_cli_rest]
|
1934
|
-
).run do |msg, data|
|
1935
|
-
case msg
|
1936
|
-
when :parse_document # once for each menu
|
1937
|
-
# puts "@ - parse document #{data}"
|
1938
|
-
ii_parse_document(data)
|
1939
|
-
|
1940
|
-
when :display_menu
|
1941
|
-
# warn "@ - display menu:"
|
1942
|
-
# ii_display_menu
|
1943
|
-
@dml_block_state = SelectedBlockMenuState.new
|
1944
|
-
@delegate_object[:block_name] = nil
|
1945
|
-
|
1946
|
-
when :user_choice
|
1947
|
-
if @dml_link_state.block_name.present?
|
1948
|
-
# @prior_block_was_link = true
|
1949
|
-
@dml_block_state.block = @dml_blocks_in_file.find do |item|
|
1950
|
-
item[:oname] == @dml_link_state.block_name
|
1951
|
-
end
|
1952
|
-
@dml_link_state.block_name = nil
|
1953
|
-
else
|
1954
|
-
# puts "? - Select a block to execute (or type #{$texit} to exit):"
|
1955
|
-
break if ii_user_choice == :break # into @dml_block_state
|
1956
|
-
break if @dml_block_state.block.nil? # no block matched
|
1957
|
-
end
|
1958
|
-
# puts "! - Executing block: #{data}"
|
1959
|
-
# @dml_block_state.block[:oname]
|
1960
|
-
@dml_block_state.block&.fetch(:oname, nil)
|
1961
|
-
|
1962
|
-
when :execute_block
|
1963
|
-
block_name = data
|
1964
|
-
if block_name == '* Back' ####
|
1965
|
-
debounce_reset
|
1966
|
-
@menu_user_clicked_back_link = true
|
1967
|
-
load_file_link_state = pop_link_history_and_trigger_load
|
1968
|
-
@dml_link_state = load_file_link_state.link_state
|
1969
|
-
|
1970
|
-
InputSequencer.merge_link_state(
|
1971
|
-
@dml_link_state,
|
1972
|
-
InputSequencer.next_link_state(
|
1973
|
-
block_name: @dml_link_state.block_name,
|
1974
|
-
document_filename: @dml_link_state.document_filename,
|
1975
|
-
prior_block_was_link: true
|
1976
|
-
)
|
1977
|
-
)
|
1978
|
-
|
1979
|
-
else
|
1980
|
-
@dml_block_state = block_state_for_name_from_cli(block_name)
|
1981
|
-
if @dml_block_state.block[:shell] == BlockType::OPTS
|
1982
|
-
debounce_reset
|
1983
|
-
link_state = LinkState.new
|
1984
|
-
options_state = read_show_options_and_trigger_reuse(
|
1985
|
-
selected: @dml_block_state.block,
|
1986
|
-
link_state: link_state
|
1987
|
-
)
|
1988
|
-
|
1989
|
-
@menu_base_options.merge!(options_state.options)
|
1990
|
-
@delegate_object.merge!(options_state.options)
|
1991
|
-
options_state.load_file_link_state.link_state
|
1992
|
-
else
|
1993
|
-
ii_execute_block(block_name)
|
1994
|
-
|
1995
|
-
if prompt_user_exit(block_name_from_cli: @run_state.block_name_from_cli,
|
1996
|
-
selected: @dml_block_state.block)
|
1997
|
-
return :break
|
1998
|
-
end
|
1999
|
-
|
2000
|
-
## order of block name processing: link block, cli, from user
|
2001
|
-
#
|
2002
|
-
@dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \
|
2003
|
-
HashDelegator.next_link_state(
|
2004
|
-
block_name: @dml_link_state.block_name,
|
2005
|
-
block_name_from_cli: !@dml_link_state.block_name.present?,
|
2006
|
-
block_state: @dml_block_state,
|
2007
|
-
was_using_cli: @dml_now_using_cli
|
2008
|
-
)
|
2009
|
-
|
2010
|
-
if !@dml_block_state.block[:block_name_from_ui] && cli_break
|
2011
|
-
# &bsp '!block_name_from_ui + cli_break -> break'
|
2012
|
-
return :break
|
2013
|
-
end
|
2014
|
-
|
2015
|
-
InputSequencer.next_link_state(
|
2016
|
-
block_name: @dml_link_state.block_name,
|
2017
|
-
prior_block_was_link: @dml_block_state.block[:shell] != BlockType::BASH
|
2018
|
-
)
|
2019
|
-
end
|
2020
|
-
end
|
2021
|
-
|
2022
|
-
when :exit?
|
2023
|
-
data == $texit
|
2024
|
-
when :stay?
|
2025
|
-
data == $stay
|
2026
|
-
else
|
2027
|
-
raise "Invalid message: #{msg}"
|
2028
|
-
end
|
2029
|
-
end
|
2030
|
-
rescue StandardError
|
2031
|
-
HashDelegator.error_handler('document_menu_loop',
|
2032
|
-
{ abort: true })
|
2033
|
-
end
|
2034
|
-
|
2035
|
-
def ii_parse_document(_document_filename)
|
2036
|
-
@run_state.batch_index += 1
|
2037
|
-
@run_state.in_own_window = false
|
2038
|
-
|
2039
|
-
# &bsp 'loop', block_name_from_cli, @cli_block_name
|
2040
|
-
@run_state.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc = \
|
2041
|
-
set_delobj_menu_loop_vars(block_name_from_cli: @run_state.block_name_from_cli,
|
2042
|
-
now_using_cli: @dml_now_using_cli,
|
2043
|
-
link_state: @dml_link_state)
|
2044
|
-
end
|
2045
|
-
|
2046
|
-
def ii_user_choice
|
2047
|
-
@dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file,
|
2048
|
-
menu_blocks: @dml_menu_blocks,
|
2049
|
-
default: @dml_menu_default_dname)
|
2050
|
-
# &bsp '@run_state.block_name_from_cli:',@run_state.block_name_from_cli
|
2051
|
-
if !@dml_block_state
|
2052
|
-
HashDelegator.error_handler('block_state missing', { abort: true })
|
2053
|
-
elsif @dml_block_state.state == MenuState::EXIT
|
2054
|
-
# &bsp 'load_cli_or_user_selected_block -> break'
|
2055
|
-
:break
|
2056
|
-
end
|
2057
|
-
end
|
2058
|
-
|
2059
|
-
def ii_execute_block(block_name)
|
2060
|
-
@dml_block_state = block_state_for_name_from_cli(block_name)
|
2061
|
-
|
2062
|
-
dump_and_warn_block_state(selected: @dml_block_state.block)
|
2063
|
-
@dml_link_state, @dml_menu_default_dname = \
|
2064
|
-
exec_bash_next_state(
|
2065
|
-
selected: @dml_block_state.block,
|
2066
|
-
mdoc: @dml_mdoc,
|
2067
|
-
link_state: @dml_link_state,
|
2068
|
-
block_source: {
|
2069
|
-
document_filename: @delegate_object[:filename],
|
2070
|
-
time_now_date: Time.now.utc.strftime(@delegate_object[:shell_code_label_time_format])
|
2071
|
-
}
|
2072
|
-
)
|
2073
|
-
end
|
2074
|
-
|
2075
|
-
def exec_bash_next_state(selected:, mdoc:, link_state:, block_source: {})
|
2076
|
-
lfls = execute_shell_type(
|
2077
|
-
selected: selected,
|
2078
|
-
mdoc: mdoc,
|
2079
|
-
link_state: link_state,
|
2080
|
-
block_source: block_source
|
2081
|
-
)
|
2082
|
-
|
2083
|
-
# if the same menu is being displayed, collect the display name of the selected menu item for use as the default item
|
2084
|
-
[lfls.link_state,
|
2085
|
-
lfls.load_file == LoadFile::Load ? nil : selected[:dname]]
|
2086
|
-
end
|
2175
|
+
(link_block_data[LinkKeys::VARS] || []).each do |(key, value)|
|
2176
|
+
ENV[key] = value.to_s
|
2177
|
+
code_lines.push(assign_key_value_in_bash(key, value))
|
2178
|
+
end
|
2179
|
+
end
|
2087
2180
|
|
2088
|
-
|
2089
|
-
|
2090
|
-
|
2091
|
-
|
2092
|
-
|
2093
|
-
|
2094
|
-
block_name_from_cli: block_name_from_cli)
|
2181
|
+
## append blocks loaded, apply LinkKeys::EVAL
|
2182
|
+
#
|
2183
|
+
if (load_expr = link_block_data.fetch(LinkKeys::LOAD, '')).present?
|
2184
|
+
load_filespec = load_filespec_from_expression(load_expr)
|
2185
|
+
code_lines += File.readlines(load_filespec, chomp: true) if load_filespec
|
2186
|
+
end
|
2095
2187
|
|
2096
|
-
#
|
2188
|
+
# if an eval link block, evaluate code_lines and return its standard output
|
2097
2189
|
#
|
2098
|
-
|
2099
|
-
|
2190
|
+
if link_block_data.fetch(LinkKeys::EVAL,
|
2191
|
+
false) || link_block_data.fetch(LinkKeys::EXEC, false)
|
2192
|
+
code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source: block_source)
|
2193
|
+
end
|
2100
2194
|
|
2101
|
-
|
2102
|
-
end
|
2195
|
+
next_document_filename = write_inherited_lines_to_file(link_state, link_block_data)
|
2103
2196
|
|
2104
|
-
|
2105
|
-
|
2106
|
-
|
2107
|
-
!block_name_from_cli &&
|
2108
|
-
selected[:shell] == BlockType::BASH &&
|
2109
|
-
@delegate_object[:pause_after_script_execution] &&
|
2110
|
-
prompt_select_continue == MenuState::EXIT
|
2111
|
-
end
|
2197
|
+
if link_block_data[LinkKeys::RETURN]
|
2198
|
+
pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines,
|
2199
|
+
dependencies, selected)
|
2112
2200
|
|
2113
|
-
|
2114
|
-
|
2115
|
-
|
2116
|
-
|
2117
|
-
|
2118
|
-
|
2119
|
-
|
2120
|
-
|
2121
|
-
|
2201
|
+
else
|
2202
|
+
link_history_push_and_next(
|
2203
|
+
curr_block_name: selected[:oname],
|
2204
|
+
curr_document_filename: @delegate_object[:filename],
|
2205
|
+
inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
|
2206
|
+
inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
|
2207
|
+
inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
|
2208
|
+
next_block_name: link_block_data.fetch(LinkKeys::NEXT_BLOCK,
|
2209
|
+
nil) || link_block_data[LinkKeys::BLOCK] || '',
|
2210
|
+
next_document_filename: next_document_filename,
|
2211
|
+
next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD
|
2212
|
+
)
|
2122
2213
|
end
|
2123
|
-
|
2124
|
-
@delegate_object = @menu_base_options.dup
|
2125
|
-
@menu_user_clicked_back_link = false
|
2126
|
-
[block_name_from_cli, now_using_cli]
|
2127
2214
|
end
|
2128
2215
|
|
2129
|
-
#
|
2130
|
-
#
|
2131
|
-
|
2132
|
-
|
2133
|
-
|
2134
|
-
|
2135
|
-
|
2136
|
-
def set_delob_filename_block_name(link_state:, block_name_from_cli:)
|
2137
|
-
@delegate_object[:filename] = link_state.document_filename
|
2138
|
-
link_state.block_name = @delegate_object[:block_name] =
|
2139
|
-
block_name_from_cli ? @cli_block_name : link_state.block_name
|
2216
|
+
# Handle expression with wildcard characters
|
2217
|
+
# allow user to select or enter
|
2218
|
+
def puts_gets_oprompt_(filespec)
|
2219
|
+
puts format(@delegate_object[:prompt_show_expr_format],
|
2220
|
+
{ expr: filespec })
|
2221
|
+
puts @delegate_object[:prompt_enter_filespec]
|
2222
|
+
gets.chomp
|
2140
2223
|
end
|
2141
2224
|
|
2142
|
-
#
|
2143
|
-
#
|
2144
|
-
# @param
|
2145
|
-
# @
|
2146
|
-
|
2147
|
-
|
2148
|
-
|
2149
|
-
|
2225
|
+
# Processes YAML data from the selected menu item, updating delegate objects and optionally printing formatted output.
|
2226
|
+
# @param selected [Hash] Selected item from the menu containing a YAML body.
|
2227
|
+
# @param tgt2 [Hash, nil] An optional target hash to update with YAML data.
|
2228
|
+
# @return [LoadFileLinkState] An instance indicating the next action for loading files.
|
2229
|
+
def read_show_options_and_trigger_reuse(selected:, link_state: LinkState.new)
|
2230
|
+
obj = {}
|
2231
|
+
data = YAML.load(selected[:body].join("\n"))
|
2232
|
+
(data || []).each do |key, value|
|
2233
|
+
sym_key = key.to_sym
|
2234
|
+
obj[sym_key] = value
|
2150
2235
|
|
2151
|
-
|
2152
|
-
warn format_and_highlight_dependencies(compact_and_index_hash(blocks_in_file),
|
2153
|
-
label: 'blocks_in_file')
|
2236
|
+
print_formatted_option(key, value) if @delegate_object[:menu_opts_set_format].present?
|
2154
2237
|
end
|
2155
2238
|
|
2156
|
-
|
2157
|
-
|
2158
|
-
|
2239
|
+
link_state.block_name = nil
|
2240
|
+
OpenStruct.new(options: obj,
|
2241
|
+
load_file_link_state: LoadFileLinkState.new(
|
2242
|
+
LoadFile::REUSE, link_state
|
2243
|
+
))
|
2244
|
+
end
|
2245
|
+
|
2246
|
+
# Check if the delegate object responds to a given method.
|
2247
|
+
# @param method_name [Symbol] The name of the method to check.
|
2248
|
+
# @param include_private [Boolean] Whether to include private methods in the check.
|
2249
|
+
# @return [Boolean] true if the delegate object responds to the method, false otherwise.
|
2250
|
+
def respond_to?(method_name, include_private = false)
|
2251
|
+
if super
|
2252
|
+
true
|
2253
|
+
elsif @delegate_object.respond_to?(method_name, include_private)
|
2254
|
+
true
|
2255
|
+
elsif method_name.to_s.end_with?('=') && @delegate_object.respond_to?(:[]=, include_private)
|
2256
|
+
true
|
2257
|
+
else
|
2258
|
+
@delegate_object.respond_to?(method_name, include_private)
|
2159
2259
|
end
|
2260
|
+
end
|
2160
2261
|
|
2161
|
-
|
2162
|
-
|
2163
|
-
|
2262
|
+
def runtime_exception(exception_sym, name, items)
|
2263
|
+
if @delegate_object[exception_sym] != 0
|
2264
|
+
data = { name: name, detail: items.join(', ') }
|
2265
|
+
warn(
|
2266
|
+
format(
|
2267
|
+
@delegate_object.fetch(:exception_format_name, "\n%{name}"),
|
2268
|
+
data
|
2269
|
+
).send(@delegate_object.fetch(:exception_color_name, :red)) +
|
2270
|
+
format(
|
2271
|
+
@delegate_object.fetch(:exception_format_detail, " - %{detail}\n"),
|
2272
|
+
data
|
2273
|
+
).send(@delegate_object.fetch(:exception_color_detail, :yellow))
|
2274
|
+
)
|
2275
|
+
end
|
2276
|
+
return unless (@delegate_object[exception_sym]).positive?
|
2164
2277
|
|
2165
|
-
|
2278
|
+
exit @delegate_object[exception_sym]
|
2166
2279
|
end
|
2167
2280
|
|
2168
|
-
|
2169
|
-
|
2170
|
-
|
2171
|
-
|
2281
|
+
# allow user to select or enter
|
2282
|
+
def save_filespec_from_expression(expression)
|
2283
|
+
# Process expression with embedded formatting
|
2284
|
+
formatted = formatted_expression(expression)
|
2285
|
+
|
2286
|
+
# Handle wildcards or direct file specification
|
2287
|
+
if contains_wildcards?(formatted)
|
2288
|
+
save_filespec_wildcard_expansion(formatted)
|
2289
|
+
else
|
2290
|
+
formatted
|
2172
2291
|
end
|
2292
|
+
end
|
2173
2293
|
|
2174
|
-
|
2294
|
+
# Handle expression with wildcard characters
|
2295
|
+
# allow user to select or enter
|
2296
|
+
def save_filespec_wildcard_expansion(filespec)
|
2297
|
+
files = find_files(filespec)
|
2298
|
+
case files.count
|
2299
|
+
when 0
|
2300
|
+
prompt_for_filespec_with_wildcard(filespec)
|
2301
|
+
else
|
2302
|
+
## user selects from existing files or other
|
2303
|
+
# input into path with wildcard for easy entry
|
2304
|
+
#
|
2305
|
+
name = prompt_select_code_filename([@delegate_object[:prompt_filespec_back], @delegate_object[:prompt_filespec_other]] + files)
|
2306
|
+
case name
|
2307
|
+
when @delegate_object[:prompt_filespec_back]
|
2308
|
+
# do nothing
|
2309
|
+
when @delegate_object[:prompt_filespec_other]
|
2310
|
+
prompt_for_filespec_with_wildcard(filespec)
|
2311
|
+
else
|
2312
|
+
name
|
2313
|
+
end
|
2314
|
+
end
|
2315
|
+
end
|
2175
2316
|
|
2176
|
-
|
2317
|
+
def save_to_file(required_lines:, selected:)
|
2318
|
+
write_command_file(required_lines: required_lines, selected: selected)
|
2319
|
+
@fout.fout "File saved: #{@run_state.saved_filespec}"
|
2177
2320
|
end
|
2178
2321
|
|
2179
2322
|
# Presents a TTY prompt to select an option or exit, returns metadata including option and selected
|
@@ -2189,20 +2332,20 @@ module MarkdownExec
|
|
2189
2332
|
selection = @prompt.select(prompt_text,
|
2190
2333
|
names,
|
2191
2334
|
opts.merge(filter: true))
|
2192
|
-
|
2335
|
+
selected_name = names.find do |item|
|
2193
2336
|
if item.instance_of?(Hash)
|
2194
2337
|
item[:dname] == selection
|
2195
2338
|
else
|
2196
2339
|
item == selection
|
2197
2340
|
end
|
2198
2341
|
end
|
2199
|
-
|
2200
|
-
unless
|
2342
|
+
selected_name = { dname: selected_name } if selected_name.instance_of?(String)
|
2343
|
+
unless selected_name
|
2201
2344
|
HashDelegator.error_handler('select_option_with_metadata', error: 'menu item not found')
|
2202
2345
|
exit 1
|
2203
2346
|
end
|
2204
2347
|
|
2205
|
-
|
2348
|
+
selected_name.merge(
|
2206
2349
|
if selection == menu_chrome_colored_option(:menu_option_back_name)
|
2207
2350
|
{ option: selection, shell: BlockType::LINK }
|
2208
2351
|
elsif selection == menu_chrome_colored_option(:menu_option_exit_name)
|
@@ -2217,6 +2360,35 @@ module MarkdownExec
|
|
2217
2360
|
HashDelegator.error_handler('select_option_with_metadata')
|
2218
2361
|
end
|
2219
2362
|
|
2363
|
+
# Update the block name in the link state and delegate object.
|
2364
|
+
#
|
2365
|
+
# This method updates the block name based on whether it was specified
|
2366
|
+
# through the CLI or derived from the link state.
|
2367
|
+
#
|
2368
|
+
# @param link_state [LinkState] The current link state object.
|
2369
|
+
# @param block_name_from_cli [Boolean] Indicates if the block name is from CLI.
|
2370
|
+
def set_delob_filename_block_name(link_state:, block_name_from_cli:)
|
2371
|
+
@delegate_object[:filename] = link_state.document_filename
|
2372
|
+
link_state.block_name = @delegate_object[:block_name] =
|
2373
|
+
block_name_from_cli ? @cli_block_name : link_state.block_name
|
2374
|
+
end
|
2375
|
+
|
2376
|
+
def set_delobj_menu_loop_vars(block_name_from_cli:, now_using_cli:, link_state:)
|
2377
|
+
block_name_from_cli, now_using_cli = \
|
2378
|
+
manage_cli_selection_state(block_name_from_cli: block_name_from_cli,
|
2379
|
+
now_using_cli: now_using_cli,
|
2380
|
+
link_state: link_state)
|
2381
|
+
set_delob_filename_block_name(link_state: link_state,
|
2382
|
+
block_name_from_cli: block_name_from_cli)
|
2383
|
+
|
2384
|
+
# update @delegate_object and @menu_base_options in auto_load
|
2385
|
+
#
|
2386
|
+
blocks_in_file, menu_blocks, mdoc = mdoc_menu_and_blocks_from_nested_files(link_state)
|
2387
|
+
dump_delobj(blocks_in_file, menu_blocks, link_state)
|
2388
|
+
|
2389
|
+
[block_name_from_cli, now_using_cli, blocks_in_file, menu_blocks, mdoc]
|
2390
|
+
end
|
2391
|
+
|
2220
2392
|
def set_environment_variables_for_block(selected)
|
2221
2393
|
code_lines = []
|
2222
2394
|
YAML.load(selected[:body].join("\n"))&.each do |key, value|
|
@@ -2352,27 +2524,6 @@ module MarkdownExec
|
|
2352
2524
|
end
|
2353
2525
|
end
|
2354
2526
|
|
2355
|
-
# Processes YAML data from the selected menu item, updating delegate objects and optionally printing formatted output.
|
2356
|
-
# @param selected [Hash] Selected item from the menu containing a YAML body.
|
2357
|
-
# @param tgt2 [Hash, nil] An optional target hash to update with YAML data.
|
2358
|
-
# @return [LoadFileLinkState] An instance indicating the next action for loading files.
|
2359
|
-
def read_show_options_and_trigger_reuse(selected:, link_state: LinkState.new)
|
2360
|
-
obj = {}
|
2361
|
-
data = YAML.load(selected[:body].join("\n"))
|
2362
|
-
(data || []).each do |key, value|
|
2363
|
-
sym_key = key.to_sym
|
2364
|
-
obj[sym_key] = value
|
2365
|
-
|
2366
|
-
print_formatted_option(key, value) if @delegate_object[:menu_opts_set_format].present?
|
2367
|
-
end
|
2368
|
-
|
2369
|
-
link_state.block_name = nil
|
2370
|
-
OpenStruct.new(options: obj,
|
2371
|
-
load_file_link_state: LoadFileLinkState.new(
|
2372
|
-
LoadFile::Reuse, link_state
|
2373
|
-
))
|
2374
|
-
end
|
2375
|
-
|
2376
2527
|
def wait_for_stream_processing
|
2377
2528
|
@process_mutex.synchronize do
|
2378
2529
|
@process_cv.wait(@process_mutex)
|
@@ -2447,14 +2598,34 @@ module MarkdownExec
|
|
2447
2598
|
HashDelegator.error_handler('write_command_file')
|
2448
2599
|
end
|
2449
2600
|
|
2601
|
+
# Ensure the directory exists before writing the file
|
2602
|
+
def write_file_with_directory_creation(save_filespec, content)
|
2603
|
+
directory = File.dirname(save_filespec)
|
2604
|
+
|
2605
|
+
begin
|
2606
|
+
FileUtils.mkdir_p(directory)
|
2607
|
+
File.write(save_filespec, content)
|
2608
|
+
rescue Errno::EACCES
|
2609
|
+
warn "Permission denied: Unable to write to file '#{save_filespec}'"
|
2610
|
+
nil
|
2611
|
+
rescue Errno::EROFS
|
2612
|
+
warn "Read-only file system: Unable to write to file '#{save_filespec}'"
|
2613
|
+
nil
|
2614
|
+
rescue StandardError => err
|
2615
|
+
warn "An error occurred while writing to file '#{save_filespec}': #{err.message}"
|
2616
|
+
nil
|
2617
|
+
end
|
2618
|
+
end
|
2619
|
+
|
2620
|
+
# return next document file name
|
2450
2621
|
def write_inherited_lines_to_file(link_state, link_block_data)
|
2451
|
-
save_expr = link_block_data.fetch(LinkKeys::
|
2622
|
+
save_expr = link_block_data.fetch(LinkKeys::SAVE, '')
|
2452
2623
|
if save_expr.present?
|
2453
2624
|
save_filespec = save_filespec_from_expression(save_expr)
|
2454
2625
|
File.write(save_filespec, HashDelegator.join_code_lines(link_state&.inherited_lines))
|
2455
2626
|
@delegate_object[:filename]
|
2456
2627
|
else
|
2457
|
-
link_block_data[LinkKeys::
|
2628
|
+
link_block_data[LinkKeys::FILE] || @delegate_object[:filename]
|
2458
2629
|
end
|
2459
2630
|
end
|
2460
2631
|
end
|
@@ -2490,21 +2661,6 @@ module MarkdownExec
|
|
2490
2661
|
|
2491
2662
|
def self.next_link_state(*args, **kwargs, &block)
|
2492
2663
|
super
|
2493
|
-
# result = super
|
2494
|
-
|
2495
|
-
# @logger ||= StdOutErrLogger.new
|
2496
|
-
# @logger.unknown(
|
2497
|
-
# HashDelegator.clean_hash_recursively(
|
2498
|
-
# { "HashDelegator.next_link_state":
|
2499
|
-
# { 'args': args,
|
2500
|
-
# 'at': Time.now.strftime('%FT%TZ'),
|
2501
|
-
# 'for': /[^\/]+:\d+/.match(caller.first)[0],
|
2502
|
-
# 'kwargs': kwargs,
|
2503
|
-
# 'return': result } }
|
2504
|
-
# )
|
2505
|
-
# )
|
2506
|
-
|
2507
|
-
# result
|
2508
2664
|
end
|
2509
2665
|
end
|
2510
2666
|
end
|
@@ -2595,21 +2751,21 @@ module MarkdownExec
|
|
2595
2751
|
|
2596
2752
|
# Test case for empty body
|
2597
2753
|
def test_push_link_history_and_trigger_load_with_empty_body
|
2598
|
-
assert_equal LoadFile::
|
2754
|
+
assert_equal LoadFile::REUSE,
|
2599
2755
|
@hd.push_link_history_and_trigger_load.load_file
|
2600
2756
|
end
|
2601
2757
|
|
2602
2758
|
# Test case for non-empty body without 'file' key
|
2603
2759
|
def test_push_link_history_and_trigger_load_without_file_key
|
2604
2760
|
body = ["vars:\n KEY: VALUE"]
|
2605
|
-
assert_equal LoadFile::
|
2761
|
+
assert_equal LoadFile::REUSE,
|
2606
2762
|
@hd.push_link_history_and_trigger_load(link_block_body: body).load_file
|
2607
2763
|
end
|
2608
2764
|
|
2609
2765
|
# Test case for non-empty body with 'file' key
|
2610
2766
|
def test_push_link_history_and_trigger_load_with_file_key
|
2611
2767
|
body = ["file: sample_file\nblock: sample_block\nvars:\n KEY: VALUE"]
|
2612
|
-
expected_result = LoadFileLinkState.new(LoadFile::
|
2768
|
+
expected_result = LoadFileLinkState.new(LoadFile::LOAD,
|
2613
2769
|
LinkState.new(block_name: 'sample_block',
|
2614
2770
|
document_filename: 'sample_file',
|
2615
2771
|
inherited_dependencies: {},
|
@@ -2647,8 +2803,9 @@ module MarkdownExec
|
|
2647
2803
|
end
|
2648
2804
|
|
2649
2805
|
def test_safeval_rescue_from_error
|
2650
|
-
|
2651
|
-
|
2806
|
+
assert_raises(SystemExit) do
|
2807
|
+
HashDelegator.safeval('invalid_code_raises_exception')
|
2808
|
+
end
|
2652
2809
|
end
|
2653
2810
|
|
2654
2811
|
def test_set_fcb_title
|
@@ -3054,7 +3211,7 @@ module MarkdownExec
|
|
3054
3211
|
|
3055
3212
|
# Asserting the result is an instance of LoadFileLinkState
|
3056
3213
|
assert_instance_of LoadFileLinkState, result
|
3057
|
-
assert_equal LoadFile::
|
3214
|
+
assert_equal LoadFile::LOAD, result.load_file
|
3058
3215
|
assert_nil result.link_state.block_name
|
3059
3216
|
end
|
3060
3217
|
end
|
@@ -3441,4 +3598,60 @@ module MarkdownExec
|
|
3441
3598
|
assert_equal expected, BashCommentFormatter.format_comment(input)
|
3442
3599
|
end
|
3443
3600
|
end
|
3601
|
+
|
3602
|
+
class PromptForFilespecWithWildcardTest < Minitest::Test
|
3603
|
+
def setup
|
3604
|
+
@delegate_object = {
|
3605
|
+
prompt_show_expr_format: 'Current expression: %{expr}',
|
3606
|
+
prompt_enter_filespec: 'Please enter a filespec:'
|
3607
|
+
}
|
3608
|
+
@original_stdin = $stdin
|
3609
|
+
end
|
3610
|
+
|
3611
|
+
def teardown
|
3612
|
+
$stdin = @original_stdin
|
3613
|
+
end
|
3614
|
+
|
3615
|
+
def test_prompt_for_filespec_with_normal_input
|
3616
|
+
$stdin = StringIO.new("test_input\n")
|
3617
|
+
result = prompt_for_filespec_with_wildcard('*.txt')
|
3618
|
+
assert_equal 'resolved_path_or_substituted_value', result
|
3619
|
+
end
|
3620
|
+
|
3621
|
+
def test_prompt_for_filespec_with_interruption
|
3622
|
+
$stdin = StringIO.new
|
3623
|
+
# rubocop disable:Lint/NestedMethodDefinition
|
3624
|
+
def $stdin.gets; raise Interrupt; end
|
3625
|
+
# rubocop enable:Lint/NestedMethodDefinition
|
3626
|
+
|
3627
|
+
result = prompt_for_filespec_with_wildcard('*.txt')
|
3628
|
+
assert_nil result
|
3629
|
+
end
|
3630
|
+
|
3631
|
+
def test_prompt_for_filespec_with_empty_input
|
3632
|
+
$stdin = StringIO.new("\n")
|
3633
|
+
result = prompt_for_filespec_with_wildcard('*.txt')
|
3634
|
+
assert_equal 'resolved_path_or_substituted_value', result
|
3635
|
+
end
|
3636
|
+
|
3637
|
+
private
|
3638
|
+
|
3639
|
+
def prompt_for_filespec_with_wildcard(filespec)
|
3640
|
+
puts format(@delegate_object[:prompt_show_expr_format], { expr: filespec })
|
3641
|
+
puts @delegate_object[:prompt_enter_filespec]
|
3642
|
+
|
3643
|
+
begin
|
3644
|
+
input = gets.chomp
|
3645
|
+
PathUtils.resolve_path_or_substitute(input, filespec)
|
3646
|
+
rescue Interrupt
|
3647
|
+
nil
|
3648
|
+
end
|
3649
|
+
end
|
3650
|
+
|
3651
|
+
module PathUtils
|
3652
|
+
def self.resolve_path_or_substitute(input, filespec)
|
3653
|
+
'resolved_path_or_substituted_value' # Placeholder implementation
|
3654
|
+
end
|
3655
|
+
end
|
3656
|
+
end
|
3444
3657
|
end # module MarkdownExec
|