markdown_exec 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -31,12 +31,15 @@ require_relative 'fout'
31
31
  require_relative 'hash'
32
32
  require_relative 'link_history'
33
33
  require_relative 'mdoc'
34
+ require_relative 'namer'
34
35
  require_relative 'regexp'
35
36
  require_relative 'resize_terminal'
36
37
  require_relative 'std_out_err_logger'
37
38
  require_relative 'streams_out'
38
39
  require_relative 'string_util'
39
40
 
41
+ $pd = false unless defined?($pd)
42
+
40
43
  class String
41
44
  # Checks if the string is not empty.
42
45
  # @return [Boolean] Returns true if the string is not empty, false otherwise.
@@ -54,7 +57,8 @@ module HashDelegatorSelf
54
57
  # @param color_key [String, Symbol] The key representing the desired color method in the color_methods hash.
55
58
  # @param default_method [String] (optional) Default color method to use if color_key is not found in color_methods. Defaults to 'plain'.
56
59
  # @return [String] The colored string.
57
- def apply_color_from_hash(string, color_methods, color_key, default_method: 'plain')
60
+ def apply_color_from_hash(string, color_methods, color_key,
61
+ default_method: 'plain')
58
62
  color_method = color_methods.fetch(color_key, default_method).to_sym
59
63
  string.to_s.send(color_method)
60
64
  end
@@ -78,17 +82,17 @@ module HashDelegatorSelf
78
82
  # colored_string = apply_color_from_hash(string, color_transformations, :red)
79
83
  # puts colored_string # This will print the string in red
80
84
 
81
- # Searches for the first element in a collection where the specified key matches a given value.
85
+ # Searches for the first element in a collection where the specified message sent to an element matches a given value.
82
86
  # This method is particularly useful for finding a specific hash-like object within an enumerable collection.
83
87
  # If no match is found, it returns a specified default value.
84
88
  #
85
89
  # @param blocks [Enumerable] The collection of hash-like objects to search.
86
- # @param key [Object] The key to search for in each element of the collection.
87
- # @param value [Object] The value to match against each element's corresponding key value.
90
+ # @param msg [Symbol, String] The message to send to each element of the collection.
91
+ # @param value [Object] The value to match against the result of the message sent to each element.
88
92
  # @param default [Object, nil] The default value to return if no match is found (optional).
89
93
  # @return [Object, nil] The first matching element or the default value if no match is found.
90
- def block_find(blocks, key, value, default = nil)
91
- blocks.find { |item| item[key] == value } || default
94
+ def block_find(blocks, msg, value, default = nil)
95
+ blocks.find { |item| item.send(msg) == value } || default
92
96
  end
93
97
 
94
98
  def code_merge(*bodies)
@@ -132,8 +136,10 @@ module HashDelegatorSelf
132
136
  # delete the current line if it is empty and the previous is also empty
133
137
  def delete_consecutive_blank_lines!(blocks_menu)
134
138
  blocks_menu.process_and_conditionally_delete! do |prev_item, current_item, _next_item|
135
- prev_item&.fetch(:chrome, nil) && !prev_item&.fetch(:oname).present? &&
136
- current_item&.fetch(:chrome, nil) && !current_item&.fetch(:oname).present?
139
+ prev_item&.fetch(:chrome, nil) &&
140
+ !(prev_item && prev_item.oname.present?) &&
141
+ current_item&.fetch(:chrome, nil) &&
142
+ !(current_item && current_item.oname.present?)
137
143
  end
138
144
  end
139
145
 
@@ -188,13 +194,14 @@ module HashDelegatorSelf
188
194
  merged.empty? ? [] : merged
189
195
  end
190
196
 
191
- def next_link_state(block_name_from_cli:, was_using_cli:, block_state:, block_name: nil)
197
+ def next_link_state(block_name_from_cli:, was_using_cli:, block_state:,
198
+ block_name: nil)
192
199
  # Set block_name based on block_name_from_cli
193
200
  block_name = @cli_block_name if block_name_from_cli
194
201
 
195
202
  # Determine the state of breaker based on was_using_cli and the block type
196
- # 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.
197
- breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block.fetch(:shell, nil) == BlockType::BASH
203
+ # 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.
204
+ breaker = !block_name && !block_name_from_cli && was_using_cli && block_state.block.shell == BlockType::BASH
198
205
 
199
206
  # Reset block_name_from_cli if the conditions are not met
200
207
  block_name_from_cli ||= false
@@ -282,7 +289,8 @@ module HashDelegatorSelf
282
289
  # @param fcb [Object] The fcb object whose attributes are to be updated.
283
290
  # @param selected_messages [Array<Symbol>] A list of message types to determine if yielding is applicable.
284
291
  # @param block [Block] An optional block to yield to if conditions are met.
285
- def update_menu_attrib_yield_selected(fcb:, messages:, configuration: {}, &block)
292
+ def update_menu_attrib_yield_selected(fcb:, messages:, configuration: {},
293
+ &block)
286
294
  initialize_fcb_names(fcb)
287
295
  return unless fcb.body
288
296
 
@@ -429,7 +437,9 @@ class StringWrapper
429
437
  words.each.with_index do |word, index|
430
438
  trial_length = word.length
431
439
  trial_length += @first_indent.length if index.zero?
432
- trial_length += current_line.length + 1 + @rest_indent.length if index != 0
440
+ if index != 0
441
+ trial_length += current_line.length + 1 + @rest_indent.length
442
+ end
433
443
  if trial_length > max_line_length && (words.count != 0)
434
444
  lines << current_line
435
445
  current_line = word
@@ -480,7 +490,8 @@ module MarkdownExec
480
490
  @most_recent_loaded_filename = nil
481
491
  @pass_args = []
482
492
  @run_state = OpenStruct.new(
483
- link_history: []
493
+ link_history: [],
494
+ source: OpenStruct.new
484
495
  )
485
496
  @link_history = LinkHistory.new
486
497
  @fout = FOut.new(@delegate_object) ### slice only relevant keys
@@ -506,13 +517,18 @@ module MarkdownExec
506
517
  def add_menu_chrome_blocks!(menu_blocks:, link_state:)
507
518
  return unless @delegate_object[:menu_link_format].present?
508
519
 
509
- add_inherited_lines(menu_blocks: menu_blocks, link_state: link_state) if @delegate_object[:menu_with_inherited_lines]
520
+ if @delegate_object[:menu_with_inherited_lines]
521
+ add_inherited_lines(menu_blocks: menu_blocks,
522
+ link_state: link_state)
523
+ end
510
524
 
511
525
  # back before exit
512
526
  add_back_option(menu_blocks: menu_blocks) if should_add_back_option?
513
527
 
514
528
  # exit after other options
515
- add_exit_option(menu_blocks: menu_blocks) if @delegate_object[:menu_with_exit]
529
+ if @delegate_object[:menu_with_exit]
530
+ add_exit_option(menu_blocks: menu_blocks)
531
+ end
516
532
 
517
533
  add_dividers(menu_blocks: menu_blocks)
518
534
  end
@@ -588,6 +604,20 @@ module MarkdownExec
588
604
  else
589
605
  menu_blocks.push(chrome_block)
590
606
  end
607
+
608
+ chrome_block
609
+ end
610
+
611
+ # Appends a formatted divider to the specified position in a menu block array.
612
+ # The method checks for the presence of formatting options before appending.
613
+ #
614
+ # @param menu_blocks [Array] The array of menu block elements.
615
+ # @param position [Symbol] The position to insert the divider (:initial or :final).
616
+ def append_divider(menu_blocks:, position:)
617
+ return unless divider_formatting_present?(position)
618
+
619
+ divider = create_divider(position)
620
+ position == :initial ? menu_blocks.unshift(divider) : menu_blocks.push(divider)
591
621
  end
592
622
 
593
623
  # Appends a formatted divider to the specified position in a menu block array.
@@ -596,10 +626,10 @@ module MarkdownExec
596
626
  # @param menu_blocks [Array] The array of menu block elements.
597
627
  # @param position [Symbol] The position to insert the divider (:initial or :final).
598
628
  def append_inherited_lines(menu_blocks:, link_state:, position: top)
599
- return unless link_state.inherited_lines.present?
629
+ return unless link_state.inherited_lines_present?
600
630
 
601
631
  insert_at_top = @delegate_object[:menu_inherited_lines_at_top]
602
- chrome_blocks = link_state.inherited_lines.map do |line|
632
+ chrome_blocks = link_state.inherited_lines_map do |line|
603
633
  formatted = format(@delegate_object[:menu_inherited_lines_format],
604
634
  { line: line })
605
635
  FCB.new(
@@ -623,18 +653,6 @@ module MarkdownExec
623
653
  HashDelegator.error_handler('append_inherited_lines')
624
654
  end
625
655
 
626
- # Appends a formatted divider to the specified position in a menu block array.
627
- # The method checks for the presence of formatting options before appending.
628
- #
629
- # @param menu_blocks [Array] The array of menu block elements.
630
- # @param position [Symbol] The position to insert the divider (:initial or :final).
631
- def append_divider(menu_blocks:, position:)
632
- return unless divider_formatting_present?(position)
633
-
634
- divider = create_divider(position)
635
- position == :initial ? menu_blocks.unshift(divider) : menu_blocks.push(divider)
636
- end
637
-
638
656
  # private
639
657
 
640
658
  # Applies shell color options to the given string if applicable.
@@ -672,7 +690,7 @@ module MarkdownExec
672
690
  iter_blocks_from_nested_files do |btype, fcb|
673
691
  process_block_based_on_type(blocks, btype, fcb)
674
692
  end
675
- # &bc 'blocks.count:', blocks.count
693
+ # &bt blocks.count
676
694
  blocks
677
695
  rescue StandardError
678
696
  HashDelegator.error_handler('blocks_from_nested_files')
@@ -684,7 +702,8 @@ module MarkdownExec
684
702
  SelectedBlockMenuState.new(
685
703
  @dml_blocks_in_file.find do |item|
686
704
  block_name == item.pub_name
687
- end&.merge(
705
+ end,
706
+ OpenStruct.new(
688
707
  block_name_from_cli: true,
689
708
  block_name_from_ui: false
690
709
  ),
@@ -698,12 +717,14 @@ module MarkdownExec
698
717
  return unless @delegate_object[:saved_stdout_folder]
699
718
 
700
719
  @delegate_object[:logged_stdout_filename] =
701
- SavedAsset.new(blockname: block_name,
702
- filename: @delegate_object[:filename],
703
- prefix: @delegate_object[:logged_stdout_filename_prefix],
704
- time: Time.now.utc,
705
- exts: '.out.txt',
706
- saved_asset_format: @delegate_object[:saved_asset_format]).generate_name
720
+ SavedAsset.new(
721
+ blockname: block_name,
722
+ filename: @delegate_object[:filename],
723
+ prefix: @delegate_object[:logged_stdout_filename_prefix],
724
+ time: Time.now.utc,
725
+ exts: '.out.txt',
726
+ saved_asset_format: shell_escape_asset_format(@dml_link_state)
727
+ ).generate_name
707
728
 
708
729
  @logged_stdout_filespec =
709
730
  @delegate_object[:logged_stdout_filespec] =
@@ -737,13 +758,14 @@ module MarkdownExec
737
758
  # @param mdoc [YourMDocClass] An instance of the MDoc class.
738
759
  # @param selected [Hash] The selected block.
739
760
  # @return [Array<String>] Required code blocks as an array of lines.
740
- def collect_required_code_lines(mdoc:, selected:, block_source:, link_state: LinkState.new)
761
+ def collect_required_code_lines(mdoc:, selected:, block_source:,
762
+ link_state: LinkState.new)
741
763
  required = mdoc.collect_recursively_required_code(
742
764
  anyname: selected.pub_name,
743
765
  label_format_above: @delegate_object[:shell_code_label_format_above],
744
766
  label_format_below: @delegate_object[:shell_code_label_format_below],
745
767
  block_source: block_source
746
- )
768
+ ) # &bt 'required'
747
769
  dependencies = (link_state&.inherited_dependencies || {}).merge(required[:dependencies] || {})
748
770
  required[:unmet_dependencies] =
749
771
  (required[:unmet_dependencies] || []) - (link_state&.inherited_block_names || [])
@@ -755,14 +777,19 @@ module MarkdownExec
755
777
  runtime_exception(:runtime_exception_error_level,
756
778
  'unmet_dependencies, flag: runtime_exception_error_level',
757
779
  required[:unmet_dependencies])
758
- else
780
+ elsif false ### use option 2024-08-02
759
781
  warn format_and_highlight_dependencies(dependencies,
760
782
  highlight: [@delegate_object[:block_name]])
761
783
  end
762
784
 
763
- code_lines = selected[:shell] == BlockType::VARS ? set_environment_variables_for_block(selected) : []
764
-
765
- HashDelegator.code_merge(link_state&.inherited_lines, required[:code] + code_lines)
785
+ if selected[:shell] == BlockType::OPTS
786
+ # body of blocks is returned as a list of lines to be read an YAML
787
+ HashDelegator.code_merge(required[:blocks].map(&:body).flatten(1))
788
+ else
789
+ code_lines = selected.shell == BlockType::VARS ? set_environment_variables_for_block(selected) : []
790
+ HashDelegator.code_merge(link_state&.inherited_lines,
791
+ required[:code] + code_lines)
792
+ end
766
793
  end
767
794
 
768
795
  def command_execute(command, args: [])
@@ -783,7 +810,8 @@ module MarkdownExec
783
810
  else
784
811
  @run_state.in_own_window = false
785
812
  execute_command_with_streams(
786
- [@delegate_object[:shell], '-c', command, @delegate_object[:filename], *args]
813
+ [@delegate_object[:shell], '-c', command,
814
+ @delegate_object[:filename], *args]
787
815
  )
788
816
  end
789
817
 
@@ -793,14 +821,16 @@ module MarkdownExec
793
821
  @run_state.aborted_at = Time.now.utc
794
822
  @run_state.error_message = err.message
795
823
  @run_state.error = err
796
- @run_state.files.append_stream_line(ExecutionStreams::STD_ERR, @run_state.error_message)
824
+ @run_state.files.append_stream_line(ExecutionStreams::STD_ERR,
825
+ @run_state.error_message)
797
826
  @fout.fout "Error ENOENT: #{err.inspect}"
798
827
  rescue SignalException => err
799
828
  # Handle SignalException
800
829
  @run_state.aborted_at = Time.now.utc
801
830
  @run_state.error_message = 'SIGTERM'
802
831
  @run_state.error = err
803
- @run_state.files.append_stream_line(ExecutionStreams::STD_ERR, @run_state.error_message)
832
+ @run_state.files.append_stream_line(ExecutionStreams::STD_ERR,
833
+ @run_state.error_message)
804
834
  @fout.fout "Error ENOENT: #{err.inspect}"
805
835
  end
806
836
 
@@ -830,18 +860,25 @@ module MarkdownExec
830
860
  # @param mdoc [Object] The markdown document object containing code blocks.
831
861
  # @param selected [Hash] The selected item from the menu to be executed.
832
862
  # @return [LoadFileLinkState] An object indicating whether to load the next block or reuse the current one.
833
- def compile_execute_and_trigger_reuse(mdoc:, selected:, block_source:, link_state: nil)
863
+ def compile_execute_and_trigger_reuse(mdoc:, selected:, block_source:,
864
+ link_state: nil)
834
865
  required_lines = collect_required_code_lines(mdoc: mdoc, selected: selected, link_state: link_state,
835
866
  block_source: block_source)
836
867
  output_or_approval = @delegate_object[:output_script] || @delegate_object[:user_must_approve]
837
- display_required_code(required_lines: required_lines) if output_or_approval
868
+ if output_or_approval
869
+ display_required_code(required_lines: required_lines)
870
+ end
838
871
  allow_execution = if @delegate_object[:user_must_approve]
839
- prompt_for_user_approval(required_lines: required_lines, selected: selected)
872
+ prompt_for_user_approval(required_lines: required_lines,
873
+ selected: selected)
840
874
  else
841
875
  true
842
876
  end
843
877
 
844
- execute_required_lines(required_lines: required_lines, selected: selected) if allow_execution
878
+ if allow_execution
879
+ execute_required_lines(required_lines: required_lines,
880
+ selected: selected)
881
+ end
845
882
 
846
883
  link_state.block_name = nil
847
884
  LoadFileLinkState.new(LoadFile::REUSE, link_state)
@@ -999,10 +1036,12 @@ module MarkdownExec
999
1036
  return true unless @delegate_object[:debounce_execution]
1000
1037
 
1001
1038
  # filter block if selected in menu
1002
- return true if @run_state.block_name_from_cli
1039
+ return true if @run_state.source.block_name_from_cli
1003
1040
 
1004
1041
  # return false if @prior_execution_block == @delegate_object[:block_name]
1005
- return @allowed_execution_block == @prior_execution_block || prompt_approve_repeat if @prior_execution_block == @delegate_object[:block_name]
1042
+ if @prior_execution_block == @delegate_object[:block_name]
1043
+ return @allowed_execution_block == @prior_execution_block || prompt_approve_repeat
1044
+ end
1006
1045
 
1007
1046
  @prior_execution_block = @delegate_object[:block_name]
1008
1047
  @allowed_execution_block = nil
@@ -1019,17 +1058,21 @@ module MarkdownExec
1019
1058
  # @param selected_option [Hash] The selected menu option.
1020
1059
  # @return [SelectedBlockMenuState] An object representing the state of the selected block.
1021
1060
  def determine_block_state(selected_option)
1022
- option_name = selected_option.fetch(:oname, nil)
1061
+ option_name = selected_option[:oname]
1023
1062
  if option_name == menu_chrome_formatted_option(:menu_option_exit_name)
1024
1063
  return SelectedBlockMenuState.new(nil,
1064
+ OpenStruct.new,
1025
1065
  MenuState::EXIT)
1026
1066
  end
1027
1067
  if option_name == menu_chrome_formatted_option(:menu_option_back_name)
1028
1068
  return SelectedBlockMenuState.new(selected_option,
1069
+ OpenStruct.new,
1029
1070
  MenuState::BACK)
1030
1071
  end
1031
1072
 
1032
- SelectedBlockMenuState.new(selected_option, MenuState::CONTINUE)
1073
+ SelectedBlockMenuState.new(selected_option,
1074
+ OpenStruct.new,
1075
+ MenuState::CONTINUE)
1033
1076
  end
1034
1077
 
1035
1078
  # Displays the required lines of code with color formatting for the preview section.
@@ -1068,9 +1111,9 @@ module MarkdownExec
1068
1111
  block_name: @delegate_object[:block_name],
1069
1112
  document_filename: @delegate_object[:filename]
1070
1113
  )
1071
- @run_state.block_name_from_cli = @dml_link_state.block_name.present?
1114
+ @run_state.source.block_name_from_cli = @dml_link_state.block_name.present?
1072
1115
  @cli_block_name = @dml_link_state.block_name
1073
- @dml_now_using_cli = @run_state.block_name_from_cli
1116
+ @dml_now_using_cli = @run_state.source.block_name_from_cli
1074
1117
  @dml_menu_default_dname = nil
1075
1118
  @dml_block_state = SelectedBlockMenuState.new
1076
1119
  @doc_saved_lines_files = []
@@ -1078,18 +1121,28 @@ module MarkdownExec
1078
1121
  ## load file with code lines per options
1079
1122
  #
1080
1123
  if @menu_base_options[:load_code].present?
1081
- @dml_link_state.inherited_lines = []
1082
- @menu_base_options[:load_code].split(':').map do |path|
1083
- @dml_link_state.inherited_lines += File.readlines(path, chomp: true)
1084
- end
1124
+ @dml_link_state.inherited_lines =
1125
+ @menu_base_options[:load_code].split(':').map do |path|
1126
+ File.readlines(path, chomp: true)
1127
+ end.flatten(1)
1085
1128
 
1086
1129
  inherited_block_names = []
1087
1130
  inherited_dependencies = {}
1088
- selected = { oname: 'load_code' }
1089
- pop_add_current_code_to_head_and_trigger_load(@dml_link_state, inherited_block_names, code_lines, inherited_dependencies, selected)
1090
- end
1091
-
1092
- fdo = ->(mo) { format(@delegate_object[:menu_link_format], HashDelegator.safeval(@delegate_object[mo])) }
1131
+ selected = FCB.new(oname: 'load_code')
1132
+ pop_add_current_code_to_head_and_trigger_load(@dml_link_state, inherited_block_names,
1133
+ code_lines, inherited_dependencies, selected)
1134
+ end
1135
+
1136
+ fdo = ->(option) {
1137
+ name = format(@delegate_object[:menu_link_format],
1138
+ HashDelegator.safeval(@delegate_object[option]))
1139
+ OpenStruct.new(
1140
+ dname: name,
1141
+ oname: name,
1142
+ name: name,
1143
+ pub_name: name.pub_name
1144
+ )
1145
+ }
1093
1146
  item_back = fdo.call(:menu_option_back_name)
1094
1147
  item_edit = fdo.call(:menu_option_edit_name)
1095
1148
  item_history = fdo.call(:menu_option_history_name)
@@ -1101,36 +1154,65 @@ module MarkdownExec
1101
1154
  @run_state.batch_random = Random.new.rand
1102
1155
  @run_state.batch_index = 0
1103
1156
 
1157
+ @run_state.files = StreamsOut.new
1158
+
1104
1159
  InputSequencer.new(
1105
1160
  @delegate_object[:filename],
1106
1161
  @delegate_object[:input_cli_rest]
1107
1162
  ).run do |msg, data|
1163
+ # &bt msg
1108
1164
  case msg
1109
1165
  when :parse_document # once for each menu
1110
1166
  # puts "@ - parse document #{data}"
1111
1167
  inpseq_parse_document(data)
1112
1168
 
1113
1169
  if @delegate_object[:menu_for_history]
1114
- history_files.tap do |files|
1115
- menu_enable_option(item_history, files.count, 'files', menu_state: MenuState::HISTORY) if files.count.positive?
1170
+ history_files(@dml_link_state).tap do |files|
1171
+ if files.count.positive?
1172
+ menu_enable_option(item_history.oname, files.count, 'files',
1173
+ menu_state: MenuState::HISTORY)
1174
+ end
1116
1175
  end
1117
1176
  end
1118
1177
 
1119
1178
  if @delegate_object[:menu_for_saved_lines] && @delegate_object[:document_saved_lines_glob].present?
1120
1179
 
1121
- sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
1180
+ sf = document_name_in_glob_as_file_name(
1181
+ @dml_link_state.document_filename,
1182
+ @delegate_object[:document_saved_lines_glob]
1183
+ )
1122
1184
  files = sf ? Dir.glob(sf) : []
1123
1185
  @doc_saved_lines_files = files.count.positive? ? files : []
1124
1186
 
1125
- lines_count = @dml_link_state.inherited_lines&.count || 0
1187
+ lines_count = @dml_link_state.inherited_lines_count
1126
1188
 
1127
1189
  # add menu items (glob, load, save) and enable selectively
1128
- menu_add_disabled_option(sf) if files.count.positive? || lines_count.positive?
1129
- menu_enable_option(item_load, files.count, 'files', menu_state: MenuState::LOAD) if files.count.positive?
1130
- menu_enable_option(item_edit, lines_count, 'lines', menu_state: MenuState::EDIT) if lines_count.positive?
1131
- menu_enable_option(item_save, 1, '', menu_state: MenuState::SAVE) if lines_count.positive?
1132
- menu_enable_option(item_view, 1, '', menu_state: MenuState::VIEW) if lines_count.positive?
1133
- menu_enable_option(item_shell, 1, '', menu_state: MenuState::SHELL) if @delegate_object[:menu_with_shell]
1190
+ if files.count.positive? || lines_count.positive?
1191
+ menu_add_disabled_option(sf)
1192
+ end
1193
+ if files.count.positive?
1194
+ menu_enable_option(item_load.dname, files.count, 'files',
1195
+ menu_state: MenuState::LOAD)
1196
+ end
1197
+ if lines_count.positive?
1198
+ menu_enable_option(item_edit.dname, lines_count, 'lines',
1199
+ menu_state: MenuState::EDIT)
1200
+ end
1201
+ if lines_count.positive?
1202
+ menu_enable_option(item_save.dname, 1, '',
1203
+ menu_state: MenuState::SAVE)
1204
+ end
1205
+ if lines_count.positive?
1206
+ menu_enable_option(item_view.dname, 1, '',
1207
+ menu_state: MenuState::VIEW)
1208
+ end
1209
+ if @delegate_object[:menu_with_shell]
1210
+ menu_enable_option(item_shell.dname, 1, '',
1211
+ menu_state: MenuState::SHELL)
1212
+ end
1213
+
1214
+ # # reflect new menu items
1215
+ # @dml_mdoc = MDoc.new(@dml_menu_blocks)
1134
1216
  end
1135
1217
 
1136
1218
  when :display_menu
@@ -1143,7 +1225,7 @@ module MarkdownExec
1143
1225
  if @dml_link_state.block_name.present?
1144
1226
  # @prior_block_was_link = true
1145
1227
  @dml_block_state.block = @dml_blocks_in_file.find do |item|
1146
- item.pub_name == @dml_link_state.block_name
1228
+ item.pub_name == @dml_link_state.block_name || item.oname == @dml_link_state.block_name
1147
1229
  end
1148
1230
  @dml_link_state.block_name = nil
1149
1231
  else
@@ -1156,7 +1238,7 @@ module MarkdownExec
1156
1238
 
1157
1239
  when :execute_block
1158
1240
  case (block_name = data)
1159
- when item_back
1241
+ when item_back.pub_name
1160
1242
  debounce_reset
1161
1243
  @menu_user_clicked_back_link = true
1162
1244
  load_file_link_state = pop_link_history_and_trigger_load
@@ -1171,64 +1253,94 @@ module MarkdownExec
1171
1253
  )
1172
1254
  )
1173
1255
 
1174
- when item_edit
1256
+ when item_edit.pub_name
1175
1257
  debounce_reset
1176
- edited = edit_text(@dml_link_state.inherited_lines.join("\n"))
1258
+ edited = edit_text(@dml_link_state.inherited_lines_block)
1177
1259
  @dml_link_state.inherited_lines = edited.split("\n") if edited
1178
1260
 
1179
1261
  return :break if pause_user_exit
1180
1262
 
1181
1263
  InputSequencer.next_link_state(prior_block_was_link: true)
1182
1264
 
1183
- when item_history
1265
+ when item_history.pub_name
1184
1266
  debounce_reset
1185
- files = history_files
1267
+ files = history_files(@dml_link_state)
1186
1268
  files_table_rows = files.map do |file|
1187
1269
  if Regexp.new(@delegate_object[:saved_asset_match]) =~ file
1188
- OpenStruct.new(file: file, row: [$~[:time], $~[:blockname], $~[:exts]].join(' '))
1270
+ begin
1271
+ OpenStruct.new(
1272
+ file: file,
1273
+ row: format(
1274
+ @delegate_object[:saved_history_format],
1275
+ # create with default '*' so unknown parameters are given a wildcard
1276
+ $~.names.each_with_object(Hash.new('*')) do |name, hash|
1277
+ hash[name.to_sym] = $~[name]
1278
+ end
1279
+ )
1280
+ )
1281
+ rescue KeyError
1282
+ # pp $!, $@
1283
+ warn "Cannot format with: #{@delegate_object[:saved_history_format]}"
1284
+ error_handler('saved_history_format')
1285
+ break
1286
+ end
1189
1287
  else
1190
1288
  warn "Cannot parse name: #{file}"
1191
1289
  next
1192
1290
  end
1193
- end.compact
1194
-
1195
- case (name = prompt_select_code_filename(
1196
- [@delegate_object[:prompt_filespec_back]] +
1197
- files_table_rows.map(&:row),
1198
- string: @delegate_object[:prompt_select_history_file],
1199
- color_sym: :prompt_color_after_script_execution
1200
- ))
1201
- when @delegate_object[:prompt_filespec_back]
1202
- # do nothing
1203
- else
1204
- file = files_table_rows.select { |ftr| ftr.row == name }&.first
1205
- info = file_info(file.file)
1206
- warn "#{file.file} - #{info[:lines]} lines / #{info[:size]} bytes"
1207
- warn(File.readlines(file.file, chomp: false).map.with_index do |line, ind|
1208
- format(' %s. %s', format('% 4d', ind).violet, line)
1209
- end)
1291
+ end&.compact
1292
+
1293
+ return :break unless files_table_rows
1294
+
1295
+ # repeat select+display until user exits
1296
+ row_attrib = :row
1297
+ loop do
1298
+ # menu with Back and Facet options at top
1299
+ case (name = prompt_select_code_filename(
1300
+ [@delegate_object[:prompt_filespec_back],
1301
+ @delegate_object[:prompt_filespec_facet]] +
1302
+ files_table_rows.map(&row_attrib),
1303
+ string: @delegate_object[:prompt_select_history_file],
1304
+ color_sym: :prompt_color_after_script_execution
1305
+ ))
1306
+ when @delegate_object[:prompt_filespec_back]
1307
+ break
1308
+ when @delegate_object[:prompt_filespec_facet]
1309
+ row_attrib = row_attrib == :row ? :file : :row
1310
+ else
1311
+ file = files_table_rows.select { |ftr| ftr.row == name }&.first
1312
+ info = file_info(file.file)
1313
+ warn "#{file.file} - #{info[:lines]} lines / #{info[:size]} bytes"
1314
+ warn(File.readlines(file.file,
1315
+ chomp: false).map.with_index do |line, ind|
1316
+ format(' %s. %s', format('% 4d', ind + 1).violet, line)
1317
+ end)
1318
+ end
1210
1319
  end
1211
1320
 
1212
1321
  return :break if pause_user_exit
1213
1322
 
1214
1323
  InputSequencer.next_link_state(prior_block_was_link: true)
1215
1324
 
1216
- when item_load
1325
+ when item_load.pub_name
1217
1326
  debounce_reset
1218
- sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
1327
+ sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename,
1328
+ @delegate_object[:document_saved_lines_glob])
1219
1329
  load_filespec = load_filespec_from_expression(sf)
1220
1330
  if load_filespec
1221
- @dml_link_state.inherited_lines ||= []
1222
- @dml_link_state.inherited_lines += File.readlines(load_filespec, chomp: true)
1331
+ @dml_link_state.inherited_lines_append(
1332
+ File.readlines(load_filespec, chomp: true)
1333
+ )
1223
1334
  end
1224
1335
 
1225
1336
  return :break if pause_user_exit
1226
1337
 
1227
1338
  InputSequencer.next_link_state(prior_block_was_link: true)
1228
1339
 
1229
- when item_save
1340
+ when item_save.pub_name
1230
1341
  debounce_reset
1231
- sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename, @delegate_object[:document_saved_lines_glob])
1342
+ sf = document_name_in_glob_as_file_name(@dml_link_state.document_filename,
1343
+ @delegate_object[:document_saved_lines_glob])
1232
1344
  save_filespec = save_filespec_from_expression(sf)
1233
1345
  if save_filespec && !write_file_with_directory_creation(
1234
1346
  save_filespec,
@@ -1240,7 +1352,7 @@ module MarkdownExec
1240
1352
 
1241
1353
  InputSequencer.next_link_state(prior_block_was_link: true)
1242
1354
 
1243
- when item_shell
1355
+ when item_shell.pub_name
1244
1356
  debounce_reset
1245
1357
  loop do
1246
1358
  command = prompt_for_command(":MDE #{Time.now.strftime('%FT%TZ')}> ".bgreen)
@@ -1261,9 +1373,9 @@ module MarkdownExec
1261
1373
 
1262
1374
  InputSequencer.next_link_state(prior_block_was_link: true)
1263
1375
 
1264
- when item_view
1376
+ when item_view.pub_name
1265
1377
  debounce_reset
1266
- warn @dml_link_state.inherited_lines.join("\n")
1378
+ warn @dml_link_state.inherited_lines_block
1267
1379
 
1268
1380
  return :break if pause_user_exit
1269
1381
 
@@ -1271,28 +1383,28 @@ module MarkdownExec
1271
1383
 
1272
1384
  else
1273
1385
  @dml_block_state = block_state_for_name_from_cli(block_name)
1274
- if @dml_block_state.block && @dml_block_state.block.fetch(:shell, nil) == BlockType::OPTS
1386
+ if @dml_block_state.block && @dml_block_state.block.shell == BlockType::OPTS
1275
1387
  debounce_reset
1276
1388
  link_state = LinkState.new
1277
1389
  options_state = read_show_options_and_trigger_reuse(
1278
- selected: @dml_block_state.block,
1279
- link_state: link_state
1390
+ link_state: link_state,
1391
+ mdoc: @dml_mdoc,
1392
+ selected: @dml_block_state.block
1280
1393
  )
1281
1394
 
1282
- @menu_base_options.merge!(options_state.options)
1283
- @delegate_object.merge!(options_state.options)
1395
+ update_menu_base(options_state.options)
1284
1396
  options_state.load_file_link_state.link_state
1285
1397
  else
1286
1398
  inpseq_execute_block(block_name)
1287
1399
 
1288
- if prompt_user_exit(block_name_from_cli: @run_state.block_name_from_cli,
1400
+ if prompt_user_exit(block_name_from_cli: @run_state.source.block_name_from_cli,
1289
1401
  selected: @dml_block_state.block)
1290
1402
  return :break
1291
1403
  end
1292
1404
 
1293
1405
  ## order of block name processing: link block, cli, from user
1294
1406
  #
1295
- @dml_link_state.block_name, @run_state.block_name_from_cli, cli_break = \
1407
+ @dml_link_state.block_name, @run_state.source.block_name_from_cli, cli_break =
1296
1408
  HashDelegator.next_link_state(
1297
1409
  block_name: @dml_link_state.block_name,
1298
1410
  block_name_from_cli: @dml_now_using_cli,
@@ -1300,14 +1412,14 @@ module MarkdownExec
1300
1412
  was_using_cli: @dml_now_using_cli
1301
1413
  )
1302
1414
 
1303
- if !@dml_block_state.block[:block_name_from_ui] && cli_break
1415
+ if !@dml_block_state.source.block_name_from_ui && cli_break
1304
1416
  # &bsp '!block_name_from_ui + cli_break -> break'
1305
1417
  return :break
1306
1418
  end
1307
1419
 
1308
1420
  InputSequencer.next_link_state(
1309
1421
  block_name: @dml_link_state.block_name,
1310
- prior_block_was_link: @dml_block_state.block.fetch(:shell, nil) != BlockType::BASH
1422
+ prior_block_was_link: @dml_block_state.block.shell != BlockType::BASH
1311
1423
  )
1312
1424
  end
1313
1425
  end
@@ -1328,9 +1440,13 @@ module MarkdownExec
1328
1440
  # remove leading "./"
1329
1441
  # replace characters: / : . * (space) with: (underscore)
1330
1442
  def document_name_in_glob_as_file_name(document_filename, glob)
1331
- return document_filename if document_filename.nil? || document_filename.empty?
1443
+ if document_filename.nil? || document_filename.empty?
1444
+ return document_filename
1445
+ end
1332
1446
 
1333
- format(glob, { document_filename: document_filename.gsub(%r{^\./}, '').gsub(/[\/:\.\* ]/, '_') })
1447
+ format(glob,
1448
+ { document_filename: document_filename.gsub(%r{^\./}, '').gsub(/[\/:\.\* ]/,
1449
+ '_') })
1334
1450
  end
1335
1451
 
1336
1452
  def dump_and_warn_block_state(selected:)
@@ -1351,7 +1467,10 @@ module MarkdownExec
1351
1467
  # @param menu_blocks [Hash] Hash of menu blocks.
1352
1468
  # @param link_state [LinkState] Current state of the link.
1353
1469
  def dump_delobj(blocks_in_file, menu_blocks, link_state)
1354
- warn format_and_highlight_hash(@delegate_object, label: '@delegate_object') if @delegate_object[:dump_delegate_object]
1470
+ if @delegate_object[:dump_delegate_object]
1471
+ warn format_and_highlight_hash(@delegate_object,
1472
+ label: '@delegate_object')
1473
+ end
1355
1474
 
1356
1475
  if @delegate_object[:dump_blocks_in_file]
1357
1476
  warn format_and_highlight_dependencies(compact_and_index_hash(blocks_in_file),
@@ -1363,11 +1482,18 @@ module MarkdownExec
1363
1482
  label: 'menu_blocks')
1364
1483
  end
1365
1484
 
1366
- warn format_and_highlight_lines(link_state.inherited_block_names, label: 'inherited_block_names') if @delegate_object[:dump_inherited_block_names]
1367
- warn format_and_highlight_lines(link_state.inherited_dependencies, label: 'inherited_dependencies') if @delegate_object[:dump_inherited_dependencies]
1485
+ if @delegate_object[:dump_inherited_block_names]
1486
+ warn format_and_highlight_lines(link_state.inherited_block_names,
1487
+ label: 'inherited_block_names')
1488
+ end
1489
+ if @delegate_object[:dump_inherited_dependencies]
1490
+ warn format_and_highlight_lines(link_state.inherited_dependencies,
1491
+ label: 'inherited_dependencies')
1492
+ end
1368
1493
  return unless @delegate_object[:dump_inherited_lines]
1369
1494
 
1370
- warn format_and_highlight_lines(link_state.inherited_lines, label: 'inherited_lines')
1495
+ warn format_and_highlight_lines(link_state.inherited_lines,
1496
+ label: 'inherited_lines')
1371
1497
  end
1372
1498
 
1373
1499
  # Opens text in an editor for user modification and returns the modified text.
@@ -1432,7 +1558,7 @@ module MarkdownExec
1432
1558
 
1433
1559
  # if the same menu is being displayed, collect the display name of the selected menu item for use as the default item
1434
1560
  [lfls.link_state,
1435
- lfls.load_file == LoadFile::LOAD ? nil : selected[:dname]]
1561
+ lfls.load_file == LoadFile::LOAD ? nil : selected.dname]
1436
1562
  #.tap { |ret| pp [__FILE__,__LINE__,'exec_bash_next_state()',ret] }
1437
1563
  end
1438
1564
 
@@ -1453,17 +1579,20 @@ module MarkdownExec
1453
1579
 
1454
1580
  Open3.popen3(*command) do |stdin, stdout, stderr, exec_thread|
1455
1581
  # Handle stdout stream
1456
- handle_stream(stream: stdout, file_type: ExecutionStreams::STD_OUT) do |line|
1582
+ handle_stream(stream: stdout,
1583
+ file_type: ExecutionStreams::STD_OUT) do |line|
1457
1584
  yield nil, line, nil, exec_thread if block_given?
1458
1585
  end
1459
1586
 
1460
1587
  # Handle stderr stream
1461
- handle_stream(stream: stderr, file_type: ExecutionStreams::STD_ERR) do |line|
1588
+ handle_stream(stream: stderr,
1589
+ file_type: ExecutionStreams::STD_ERR) do |line|
1462
1590
  yield nil, nil, line, exec_thread if block_given?
1463
1591
  end
1464
1592
 
1465
1593
  # Handle stdin stream
1466
- input_thread = handle_stream(stream: $stdin, file_type: ExecutionStreams::STD_IN) do |line|
1594
+ input_thread = handle_stream(stream: $stdin,
1595
+ file_type: ExecutionStreams::STD_IN) do |line|
1467
1596
  stdin.puts(line)
1468
1597
  yield line, nil, nil, exec_thread if block_given?
1469
1598
  end
@@ -1490,8 +1619,13 @@ module MarkdownExec
1490
1619
  # @param required_lines [Array<String>] The lines of code to be executed.
1491
1620
  # @param selected [FCB] The selected functional code block object.
1492
1621
  def execute_required_lines(required_lines: [], selected: FCB.new)
1493
- write_command_file(required_lines: required_lines, selected: selected) if @delegate_object[:save_executed_script]
1494
- calc_logged_stdout_filename(block_name: @dml_block_state.block[:oname]) if @dml_block_state
1622
+ if @delegate_object[:save_executed_script]
1623
+ write_command_file(required_lines: required_lines,
1624
+ selected: selected)
1625
+ end
1626
+ if @dml_block_state
1627
+ calc_logged_stdout_filename(block_name: @dml_block_state.block.oname)
1628
+ end
1495
1629
  format_and_execute_command(code_lines: required_lines)
1496
1630
  post_execution_process
1497
1631
  end
@@ -1505,10 +1639,11 @@ module MarkdownExec
1505
1639
  # @param opts [Hash] Options hash containing configuration settings.
1506
1640
  # @param mdoc [YourMDocClass] An instance of the MDoc class.
1507
1641
  #
1508
- def execute_shell_type(selected:, mdoc:, block_source:, link_state: LinkState.new)
1509
- if selected.fetch(:shell, '') == BlockType::LINK
1642
+ def execute_shell_type(selected:, mdoc:, block_source:,
1643
+ link_state: LinkState.new)
1644
+ if selected.shell == BlockType::LINK
1510
1645
  debounce_reset
1511
- push_link_history_and_trigger_load(link_block_body: selected.fetch(:body, ''),
1646
+ push_link_history_and_trigger_load(link_block_body: selected.body,
1512
1647
  mdoc: mdoc,
1513
1648
  selected: selected,
1514
1649
  link_state: link_state,
@@ -1518,46 +1653,34 @@ module MarkdownExec
1518
1653
  debounce_reset
1519
1654
  pop_link_history_and_trigger_load
1520
1655
 
1521
- elsif selected[:shell] == BlockType::OPTS
1656
+ elsif selected.shell == BlockType::OPTS
1522
1657
  debounce_reset
1523
- block_names = []
1524
1658
  code_lines = []
1525
- dependencies = {}
1526
- options_state = read_show_options_and_trigger_reuse(selected: selected, link_state: link_state)
1527
-
1528
- ## apply options to current state
1529
- #
1530
- @menu_base_options.merge!(options_state.options)
1531
- @delegate_object.merge!(options_state.options)
1659
+ options_state = read_show_options_and_trigger_reuse(
1660
+ link_state: link_state,
1661
+ mdoc: @dml_mdoc,
1662
+ selected: selected
1663
+ )
1664
+ update_menu_base(options_state.options)
1532
1665
 
1533
1666
  ### options_state.load_file_link_state
1534
1667
  link_state = LinkState.new
1535
- link_history_push_and_next(
1536
- curr_block_name: selected.pub_name,
1537
- curr_document_filename: @delegate_object[:filename],
1538
- inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1539
- inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
1540
- inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
1541
- next_block_name: '',
1542
- next_document_filename: @delegate_object[:filename],
1543
- next_load_file: LoadFile::REUSE
1544
- )
1668
+ next_state_append_code(selected, link_state, code_lines)
1545
1669
 
1546
- elsif selected[:shell] == BlockType::VARS
1670
+ elsif selected.shell == BlockType::PORT
1547
1671
  debounce_reset
1548
- block_names = []
1549
- code_lines = set_environment_variables_for_block(selected)
1550
- dependencies = {}
1551
- link_history_push_and_next(
1552
- curr_block_name: selected.pub_name,
1553
- curr_document_filename: @delegate_object[:filename],
1554
- inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
1555
- inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
1556
- inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
1557
- next_block_name: '',
1558
- next_document_filename: @delegate_object[:filename],
1559
- next_load_file: LoadFile::REUSE
1672
+ required_lines = collect_required_code_lines(
1673
+ mdoc: @dml_mdoc,
1674
+ selected: selected,
1675
+ link_state: link_state,
1676
+ block_source: block_source
1560
1677
  )
1678
+ next_state_set_code(selected, link_state, required_lines)
1679
+
1680
+ elsif selected.shell == BlockType::VARS
1681
+ debounce_reset
1682
+ next_state_append_code(selected, link_state,
1683
+ set_environment_variables_for_block(selected))
1561
1684
 
1562
1685
  elsif debounce_allows
1563
1686
  compile_execute_and_trigger_reuse(mdoc: mdoc,
@@ -1646,6 +1769,16 @@ module MarkdownExec
1646
1769
  expr.include?('%{') ? format_expression(expr) : expr
1647
1770
  end
1648
1771
 
1772
+ def generate_temp_filename(ext = '.sh')
1773
+ filename = begin
1774
+ Dir::Tmpname.make_tmpname(['x', ext], nil)
1775
+ rescue NoMethodError
1776
+ require 'securerandom'
1777
+ "#{SecureRandom.urlsafe_base64}#{ext}"
1778
+ end
1779
+ File.join(Dir.tmpdir, filename)
1780
+ end
1781
+
1649
1782
  # Processes a block to generate its summary, modifying its attributes based on various matching criteria.
1650
1783
  # It handles special formatting for bash blocks, extracting and setting properties like call, stdin, stdout, and dname.
1651
1784
  #
@@ -1659,7 +1792,7 @@ module MarkdownExec
1659
1792
  bm = extract_named_captures_from_option(titlexcall,
1660
1793
  @delegate_object[:block_name_match])
1661
1794
 
1662
- shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
1795
+ shell_color_option = SHELL_COLOR_OPTIONS[fcb.shell]
1663
1796
 
1664
1797
  if @delegate_object[:block_name_nick_match].present? && fcb.oname =~ Regexp.new(@delegate_object[:block_name_nick_match])
1665
1798
  fcb.nickname = $~[0]
@@ -1670,9 +1803,10 @@ module MarkdownExec
1670
1803
 
1671
1804
  fcb.dname = HashDelegator.indent_all_lines(
1672
1805
  apply_shell_color_option(fcb.oname, shell_color_option),
1673
- fcb.fetch(:indent, nil)
1806
+ fcb.indent
1674
1807
  )
1675
- fcb
1808
+
1809
+ fcb # &br
1676
1810
  end
1677
1811
 
1678
1812
  # Updates the delegate object's state based on the provided block state.
@@ -1695,13 +1829,13 @@ module MarkdownExec
1695
1829
  Thread.new do
1696
1830
  stream.each_line do |line|
1697
1831
  line.strip!
1698
- @run_state.files.append_stream_line(file_type, line) if @run_state.files.streams
1699
-
1700
- if @delegate_object[:output_stdout]
1701
- # print line
1702
- puts line
1832
+ if @run_state.files.streams
1833
+ @run_state.files.append_stream_line(file_type,
1834
+ line)
1703
1835
  end
1704
1836
 
1837
+ puts line if @delegate_object[:output_stdout]
1838
+
1705
1839
  yield line if block_given?
1706
1840
  end
1707
1841
  rescue IOError
@@ -1712,25 +1846,40 @@ module MarkdownExec
1712
1846
  end
1713
1847
  end
1714
1848
 
1715
- def history_files
1716
- Dir.glob(
1849
+ def history_files(link_state, order: :chronological, direction: :reverse)
1850
+ files = Dir.glob(
1717
1851
  File.join(
1718
1852
  @delegate_object[:saved_script_folder],
1719
- SavedAsset.new(filename: @delegate_object[:filename],
1720
- saved_asset_format: @delegate_object[:saved_asset_format]).generate_name
1853
+ SavedAsset.new(
1854
+ filename: @delegate_object[:filename],
1855
+ saved_asset_format: shell_escape_asset_format(link_state)
1856
+ ).generate_name
1721
1857
  )
1722
1858
  )
1859
+
1860
+ sorted_files = case order
1861
+ when :alphabetical
1862
+ files.sort
1863
+ when :chronological
1864
+ files.sort_by { |file| File.mtime(file) }
1865
+ else
1866
+ raise ArgumentError, "Invalid order: #{order}"
1867
+ end
1868
+
1869
+ direction == :reverse ? sorted_files.reverse : sorted_files
1723
1870
  end
1724
1871
 
1725
1872
  # Initializes variables for regex and other states
1726
1873
  def initial_state
1727
1874
  {
1728
- fenced_start_and_end_regex: Regexp.new(@delegate_object.fetch(
1729
- :fenced_start_and_end_regex, '^(?<indent> *)`{3,}'
1730
- )),
1731
- fenced_start_extended_regex: Regexp.new(@delegate_object.fetch(
1732
- :fenced_start_and_end_regex, '^(?<indent> *)`{3,}'
1733
- )),
1875
+ fenced_start_and_end_regex:
1876
+ Regexp.new(@delegate_object.fetch(
1877
+ :fenced_start_and_end_regex, '^(?<indent> *)`{3,}'
1878
+ )),
1879
+ fenced_start_extended_regex:
1880
+ Regexp.new(@delegate_object.fetch(
1881
+ :fenced_start_and_end_regex, '^(?<indent> *)`{3,}'
1882
+ )),
1734
1883
  fcb: MarkdownExec::FCB.new,
1735
1884
  in_fenced_block: false,
1736
1885
  headings: []
@@ -1740,7 +1889,7 @@ module MarkdownExec
1740
1889
  def inpseq_execute_block(block_name)
1741
1890
  @dml_block_state = block_state_for_name_from_cli(block_name)
1742
1891
  dump_and_warn_block_state(selected: @dml_block_state.block)
1743
- @dml_link_state, @dml_menu_default_dname = \
1892
+ @dml_link_state, @dml_menu_default_dname =
1744
1893
  exec_bash_next_state(
1745
1894
  selected: @dml_block_state.block,
1746
1895
  mdoc: @dml_mdoc,
@@ -1757,8 +1906,8 @@ module MarkdownExec
1757
1906
  @run_state.in_own_window = false
1758
1907
 
1759
1908
  # &bsp 'loop', block_name_from_cli, @cli_block_name
1760
- @run_state.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc = \
1761
- set_delobj_menu_loop_vars(block_name_from_cli: @run_state.block_name_from_cli,
1909
+ @run_state.source.block_name_from_cli, @dml_now_using_cli, @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc =
1910
+ set_delobj_menu_loop_vars(block_name_from_cli: @run_state.source.block_name_from_cli,
1762
1911
  now_using_cli: @dml_now_using_cli,
1763
1912
  link_state: @dml_link_state)
1764
1913
  end
@@ -1767,7 +1916,7 @@ module MarkdownExec
1767
1916
  @dml_block_state = load_cli_or_user_selected_block(all_blocks: @dml_blocks_in_file,
1768
1917
  menu_blocks: @dml_menu_blocks,
1769
1918
  default: @dml_menu_default_dname)
1770
- # &bsp '@run_state.block_name_from_cli:',@run_state.block_name_from_cli
1919
+ # &bsp '@run_state.source.block_name_from_cli:',@run_state.source.block_name_from_cli
1771
1920
  if !@dml_block_state
1772
1921
  HashDelegator.error_handler('block_state missing', { abort: true })
1773
1922
  elsif @dml_block_state.state == MenuState::EXIT
@@ -1793,8 +1942,10 @@ module MarkdownExec
1793
1942
  end
1794
1943
  end
1795
1944
 
1796
- def link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source:)
1797
- all_code = HashDelegator.code_merge(link_state&.inherited_lines, code_lines)
1945
+ def link_block_data_eval(link_state, code_lines, selected, link_block_data,
1946
+ block_source:)
1947
+ all_code = HashDelegator.code_merge(link_state&.inherited_lines,
1948
+ code_lines)
1798
1949
  output_lines = []
1799
1950
 
1800
1951
  Tempfile.open do |file|
@@ -1813,7 +1964,8 @@ module MarkdownExec
1813
1964
  #
1814
1965
  output_lines = process_string_array(
1815
1966
  output_lines,
1816
- begin_pattern: @delegate_object.fetch(:output_assignment_begin, nil),
1967
+ begin_pattern: @delegate_object.fetch(:output_assignment_begin,
1968
+ nil),
1817
1969
  end_pattern: @delegate_object.fetch(:output_assignment_end, nil),
1818
1970
  scan1: @delegate_object.fetch(:output_assignment_match, nil),
1819
1971
  format1: @delegate_object.fetch(:output_assignment_format, nil)
@@ -1824,7 +1976,10 @@ module MarkdownExec
1824
1976
  end
1825
1977
  end
1826
1978
 
1827
- HashDelegator.error_handler('all_code eval output_lines is nil', { abort: true }) unless output_lines
1979
+ unless output_lines
1980
+ HashDelegator.error_handler('all_code eval output_lines is nil',
1981
+ { abort: true })
1982
+ end
1828
1983
 
1829
1984
  label_format_above = @delegate_object[:shell_code_label_format_above]
1830
1985
  label_format_below = @delegate_object[:shell_code_label_format_below]
@@ -1833,7 +1988,11 @@ module MarkdownExec
1833
1988
  block_source.merge({ block_name: selected.pub_name }))] +
1834
1989
  output_lines.map do |line|
1835
1990
  re = Regexp.new(link_block_data.fetch('pattern', '(?<line>.*)'))
1836
- re.gsub_format(line, link_block_data.fetch('format', '%{line}')) if re =~ line
1991
+ next unless re =~ line
1992
+
1993
+ re.gsub_format(line,
1994
+ link_block_data.fetch('format',
1995
+ '%{line}'))
1837
1996
  end.compact +
1838
1997
  [label_format_below && format(label_format_below,
1839
1998
  block_source.merge({ block_name: selected.pub_name }))]
@@ -1882,34 +2041,41 @@ module MarkdownExec
1882
2041
  # Executes a specified block once per filename.
1883
2042
  # @param all_blocks [Array] Array of all block elements.
1884
2043
  # @return [Boolean, nil] True if values were modified, nil otherwise.
1885
- def load_auto_opts_block(all_blocks)
2044
+ def load_auto_opts_block(all_blocks, mdoc:)
1886
2045
  block_name = @delegate_object[:document_load_opts_block_name]
1887
- return unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename]
2046
+ unless block_name.present? && @most_recent_loaded_filename != @delegate_object[:filename]
2047
+ return
2048
+ end
1888
2049
 
1889
2050
  block = HashDelegator.block_find(all_blocks, :oname, block_name)
1890
2051
  return unless block
1891
2052
 
1892
- options_state = read_show_options_and_trigger_reuse(selected: block)
1893
- @menu_base_options.merge!(options_state.options)
1894
- @delegate_object.merge!(options_state.options)
2053
+ options_state = read_show_options_and_trigger_reuse(
2054
+ mdoc: mdoc,
2055
+ selected: block
2056
+ )
2057
+ update_menu_base(options_state.options)
1895
2058
 
1896
2059
  @most_recent_loaded_filename = @delegate_object[:filename]
1897
2060
  true
1898
2061
  end
1899
2062
 
1900
- def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [], default: nil)
2063
+ def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [],
2064
+ default: nil)
1901
2065
  if @delegate_object[:block_name].present?
1902
2066
  block = all_blocks.find do |item|
1903
2067
  item.pub_name == @delegate_object[:block_name]
1904
- end&.merge(block_name_from_ui: false)
2068
+ end
2069
+ source = OpenStruct.new(block_name_from_ui: false)
1905
2070
  else
1906
2071
  block_state = wait_for_user_selected_block(all_blocks, menu_blocks,
1907
2072
  default)
1908
- block = block_state.block&.merge(block_name_from_ui: true)
2073
+ block = block_state.block
2074
+ source = OpenStruct.new(block_name_from_ui: true)
1909
2075
  state = block_state.state
1910
2076
  end
1911
2077
 
1912
- SelectedBlockMenuState.new(block, state)
2078
+ SelectedBlockMenuState.new(block, source, state)
1913
2079
  rescue StandardError
1914
2080
  HashDelegator.error_handler('load_cli_or_user_selected_block')
1915
2081
  end
@@ -1932,7 +2098,8 @@ module MarkdownExec
1932
2098
  def load_filespec_wildcard_expansion(expr, auto_load_single: false)
1933
2099
  files = find_files(expr)
1934
2100
  if files.count.zero?
1935
- HashDelegator.error_handler("no files found with '#{expr}' ", { abort: true })
2101
+ HashDelegator.error_handler("no files found with '#{expr}' ",
2102
+ { abort: true })
1936
2103
  elsif auto_load_single && files.count == 1
1937
2104
  files.first
1938
2105
  else
@@ -1966,20 +2133,22 @@ module MarkdownExec
1966
2133
 
1967
2134
  # recreate menu with new options
1968
2135
  #
1969
- all_blocks, mdoc = mdoc_and_blocks_from_nested_files if load_auto_opts_block(all_blocks)
2136
+ all_blocks, mdoc = mdoc_and_blocks_from_nested_files if load_auto_opts_block(
2137
+ all_blocks, mdoc: mdoc
2138
+ )
1970
2139
 
1971
2140
  menu_blocks = mdoc.fcbs_per_options(@delegate_object)
1972
2141
  add_menu_chrome_blocks!(menu_blocks: menu_blocks, link_state: link_state)
1973
2142
  ### compress empty lines
1974
2143
  HashDelegator.delete_consecutive_blank_lines!(menu_blocks)
1975
- [all_blocks, menu_blocks, mdoc]
2144
+ [all_blocks, menu_blocks, mdoc] # &br
1976
2145
  end
1977
2146
 
1978
2147
  def menu_add_disabled_option(name)
1979
2148
  raise unless name.present?
1980
2149
  raise if @dml_menu_blocks.nil?
1981
2150
 
1982
- block = @dml_menu_blocks.find { |item| item[:oname] == name }
2151
+ block = @dml_menu_blocks.find { |item| item.oname == name }
1983
2152
 
1984
2153
  # create menu item when it is needed (count > 0)
1985
2154
  #
@@ -2017,7 +2186,9 @@ module MarkdownExec
2017
2186
  # @param option_symbol [Symbol] The symbol key for the menu option in the delegate object.
2018
2187
  # @return [String] The formatted or original value of the menu option.
2019
2188
  def menu_chrome_formatted_option(option_symbol = :menu_option_back_name)
2020
- option_value = HashDelegator.safeval(@delegate_object.fetch(option_symbol, ''))
2189
+ option_value = HashDelegator.safeval(@delegate_object.fetch(
2190
+ option_symbol, ''
2191
+ ))
2021
2192
 
2022
2193
  if @delegate_object[:menu_chrome_format]
2023
2194
  format(@delegate_object[:menu_chrome_format], option_value)
@@ -2030,20 +2201,20 @@ module MarkdownExec
2030
2201
  raise unless name.present?
2031
2202
  raise if @dml_menu_blocks.nil?
2032
2203
 
2033
- item = @dml_menu_blocks.find { |block| block[:oname] == name }
2204
+ item = @dml_menu_blocks.find { |block| block.oname == name }
2034
2205
 
2035
2206
  # create menu item when it is needed (count > 0)
2036
2207
  #
2037
2208
  if item.nil? && count.positive?
2038
- append_chrome_block(menu_blocks: @dml_menu_blocks, menu_state: menu_state)
2039
- item = @dml_menu_blocks.find { |block| block[:oname] == name }
2209
+ item = append_chrome_block(menu_blocks: @dml_menu_blocks,
2210
+ menu_state: menu_state)
2040
2211
  end
2041
2212
 
2042
2213
  # update item if it exists
2043
2214
  #
2044
2215
  return unless item
2045
2216
 
2046
- item[:dname] = type.present? ? "#{name} (#{count} #{type})" : name
2217
+ item.dname = type.present? ? "#{name} (#{count} #{type})" : name
2047
2218
  if count.positive?
2048
2219
  item.delete(:disabled)
2049
2220
  else
@@ -2051,14 +2222,15 @@ module MarkdownExec
2051
2222
  end
2052
2223
  end
2053
2224
 
2054
- def manage_cli_selection_state(block_name_from_cli:, now_using_cli:, link_state:)
2225
+ def manage_cli_selection_state(block_name_from_cli:, now_using_cli:,
2226
+ link_state:)
2055
2227
  if block_name_from_cli && @cli_block_name == @menu_base_options[:menu_persist_block_name]
2056
2228
  # &bsp 'pause cli control, allow user to select block'
2057
2229
  block_name_from_cli = false
2058
2230
  now_using_cli = false
2059
- @menu_base_options[:block_name] = \
2231
+ @menu_base_options[:block_name] =
2060
2232
  @delegate_object[:block_name] = \
2061
- link_state.block_name = \
2233
+ link_state.block_name =
2062
2234
  @cli_block_name = nil
2063
2235
  end
2064
2236
 
@@ -2079,6 +2251,27 @@ module MarkdownExec
2079
2251
  end
2080
2252
  end
2081
2253
 
2254
+ def next_state_append_code(selected, link_state, code_lines)
2255
+ next_state_set_code(selected, link_state, HashDelegator.code_merge(
2256
+ link_state&.inherited_lines, code_lines
2257
+ ))
2258
+ end
2259
+
2260
+ def next_state_set_code(selected, link_state, code_lines)
2261
+ block_names = []
2262
+ dependencies = {}
2263
+ link_history_push_and_next(
2264
+ curr_block_name: selected.pub_name,
2265
+ curr_document_filename: @delegate_object[:filename],
2266
+ inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
2267
+ inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
2268
+ inherited_lines: HashDelegator.code_merge(code_lines),
2269
+ next_block_name: '',
2270
+ next_document_filename: @delegate_object[:filename],
2271
+ next_load_file: LoadFile::REUSE
2272
+ )
2273
+ end
2274
+
2082
2275
  def output_color_formatted(data_sym, color_sym)
2083
2276
  formatted_string = string_send_color(@delegate_object[data_sym],
2084
2277
  color_sym)
@@ -2159,9 +2352,12 @@ module MarkdownExec
2159
2352
  link_history_push_and_next(
2160
2353
  curr_block_name: selected.pub_name,
2161
2354
  curr_document_filename: @delegate_object[:filename],
2162
- inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
2163
- inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
2164
- inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
2355
+ inherited_block_names:
2356
+ ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
2357
+ inherited_dependencies:
2358
+ (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
2359
+ inherited_lines:
2360
+ HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
2165
2361
  next_block_name: next_block_name,
2166
2362
  next_document_filename: @delegate_object[:filename], # not next_document_filename
2167
2363
  next_load_file: LoadFile::REUSE # not next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD
@@ -2177,12 +2373,15 @@ module MarkdownExec
2177
2373
  def pop_link_history_and_trigger_load
2178
2374
  pop = @link_history.pop
2179
2375
  peek = @link_history.peek
2180
- LoadFileLinkState.new(LoadFile::LOAD, LinkState.new(
2181
- document_filename: pop.document_filename,
2182
- inherited_block_names: peek.inherited_block_names,
2183
- inherited_dependencies: peek.inherited_dependencies,
2184
- inherited_lines: peek.inherited_lines
2185
- ))
2376
+ LoadFileLinkState.new(
2377
+ LoadFile::LOAD,
2378
+ LinkState.new(
2379
+ document_filename: pop.document_filename,
2380
+ inherited_block_names: peek.inherited_block_names,
2381
+ inherited_dependencies: peek.inherited_dependencies,
2382
+ inherited_lines: peek.inherited_lines
2383
+ )
2384
+ )
2186
2385
  end
2187
2386
 
2188
2387
  def post_execution_process
@@ -2198,20 +2397,21 @@ module MarkdownExec
2198
2397
  # @return [Array<Hash>] The updated blocks menu.
2199
2398
  def prepare_blocks_menu(menu_blocks)
2200
2399
  menu_blocks.map do |fcb|
2201
- next if Filter.prepared_not_in_menu?(@delegate_object, fcb,
2202
- %i[block_name_include_match block_name_wrapper_match])
2400
+ next if Filter.prepared_not_in_menu?(
2401
+ @delegate_object,
2402
+ fcb,
2403
+ %i[block_name_include_match block_name_wrapper_match]
2404
+ )
2203
2405
 
2204
- fcb.merge!(
2205
- name: fcb.dname,
2206
- label: BlockLabel.make(
2207
- body: fcb[:body],
2208
- filename: @delegate_object[:filename],
2209
- headings: fcb.fetch(:headings, []),
2210
- menu_blocks_with_docname: @delegate_object[:menu_blocks_with_docname],
2211
- menu_blocks_with_headings: @delegate_object[:menu_blocks_with_headings],
2212
- text: fcb[:text],
2213
- title: fcb[:title]
2214
- )
2406
+ fcb.name = fcb.dname
2407
+ fcb.label = BlockLabel.make(
2408
+ body: fcb.body,
2409
+ filename: @delegate_object[:filename],
2410
+ headings: fcb.headings,
2411
+ menu_blocks_with_docname: @delegate_object[:menu_blocks_with_docname],
2412
+ menu_blocks_with_headings: @delegate_object[:menu_blocks_with_headings],
2413
+ text: fcb.text,
2414
+ title: fcb.title
2215
2415
  )
2216
2416
  fcb.to_h
2217
2417
  end.compact
@@ -2232,7 +2432,10 @@ module MarkdownExec
2232
2432
  when :filter
2233
2433
  %i[blocks line]
2234
2434
  when :line
2235
- create_and_add_chrome_blocks(blocks, fcb) unless @delegate_object[:no_chrome]
2435
+ unless @delegate_object[:no_chrome]
2436
+ create_and_add_chrome_blocks(blocks,
2437
+ fcb)
2438
+ end
2236
2439
  end
2237
2440
  end
2238
2441
 
@@ -2305,7 +2508,8 @@ module MarkdownExec
2305
2508
  # @param filespec [String] the wildcard expression to be substituted
2306
2509
  # @return [String, nil] the resolved path or substituted expression, or nil if interrupted
2307
2510
  def prompt_for_filespec_with_wildcard(filespec)
2308
- puts format(@delegate_object[:prompt_show_expr_format], { expr: filespec })
2511
+ puts format(@delegate_object[:prompt_show_expr_format],
2512
+ { expr: filespec })
2309
2513
  puts @delegate_object[:prompt_enter_filespec]
2310
2514
 
2311
2515
  begin
@@ -2362,26 +2566,40 @@ module MarkdownExec
2362
2566
 
2363
2567
  # public
2364
2568
 
2365
- def prompt_select_code_filename(filenames, string: @delegate_object[:prompt_select_code_file], color_sym: :prompt_color_after_script_execution)
2569
+ def prompt_select_code_filename(
2570
+ filenames,
2571
+ color_sym: :prompt_color_after_script_execution,
2572
+ cycle: true,
2573
+ enum: false,
2574
+ quiet: true,
2575
+ string: @delegate_object[:prompt_select_code_file]
2576
+ )
2366
2577
  @prompt.select(
2367
2578
  string_send_color(string, color_sym),
2368
- filter: true,
2369
- quiet: true
2579
+ cycle: cycle,
2580
+ filter: !enum,
2581
+ per_page: @delegate_object[:select_page_height],
2582
+ quiet: quiet
2370
2583
  ) do |menu|
2371
- filenames.each do |filename|
2372
- menu.choice filename
2584
+ menu.enum '.' if enum
2585
+ filenames.each.with_index do |filename, ind|
2586
+ if enum
2587
+ menu.choice filename, ind + 1
2588
+ else
2589
+ menu.choice filename
2590
+ end
2373
2591
  end
2374
2592
  end
2375
2593
  rescue TTY::Reader::InputInterrupt
2376
2594
  exit 1
2377
2595
  end
2378
2596
 
2379
- def prompt_select_continue
2597
+ def prompt_select_continue(filter: true, quiet: true)
2380
2598
  sel = @prompt.select(
2381
2599
  string_send_color(@delegate_object[:prompt_after_script_execution],
2382
2600
  :prompt_color_after_script_execution),
2383
- filter: true,
2384
- quiet: true
2601
+ filter: filter,
2602
+ quiet: quiet
2385
2603
  ) do |menu|
2386
2604
  menu.choice @delegate_object[:prompt_yes]
2387
2605
  menu.choice @delegate_object[:prompt_exit]
@@ -2394,7 +2612,7 @@ module MarkdownExec
2394
2612
  # user prompt to exit if the menu will be displayed again
2395
2613
  #
2396
2614
  def prompt_user_exit(block_name_from_cli:, selected:)
2397
- selected[:shell] == BlockType::BASH &&
2615
+ selected.shell == BlockType::BASH &&
2398
2616
  @delegate_object[:pause_after_script_execution] &&
2399
2617
  prompt_select_continue == MenuState::EXIT
2400
2618
  end
@@ -2443,18 +2661,26 @@ module MarkdownExec
2443
2661
  #
2444
2662
  if (load_expr = link_block_data.fetch(LinkKeys::LOAD, '')).present?
2445
2663
  load_filespec = load_filespec_from_expression(load_expr)
2446
- code_lines += File.readlines(load_filespec, chomp: true) if load_filespec
2664
+ if load_filespec
2665
+ code_lines += File.readlines(load_filespec,
2666
+ chomp: true)
2667
+ end
2447
2668
  end
2448
2669
 
2449
2670
  # if an eval link block, evaluate code_lines and return its standard output
2450
2671
  #
2451
2672
  if link_block_data.fetch(LinkKeys::EVAL,
2452
- false) || link_block_data.fetch(LinkKeys::EXEC, false)
2453
- code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data, block_source: block_source)
2673
+ false) || link_block_data.fetch(LinkKeys::EXEC,
2674
+ false)
2675
+ code_lines = link_block_data_eval(link_state, code_lines, selected, link_block_data,
2676
+ block_source: block_source)
2454
2677
  end
2455
2678
 
2456
- next_document_filename = write_inherited_lines_to_file(link_state, link_block_data)
2457
- next_block_name = link_block_data.fetch(LinkKeys::NEXT_BLOCK, nil) || link_block_data.fetch(LinkKeys::BLOCK, nil) || ''
2679
+ next_document_filename = write_inherited_lines_to_file(link_state,
2680
+ link_block_data)
2681
+ next_block_name = link_block_data.fetch(LinkKeys::NEXT_BLOCK,
2682
+ nil) || link_block_data.fetch(LinkKeys::BLOCK,
2683
+ nil) || ''
2458
2684
 
2459
2685
  if link_block_data[LinkKeys::RETURN]
2460
2686
  pop_add_current_code_to_head_and_trigger_load(link_state, block_names, code_lines,
@@ -2466,7 +2692,9 @@ module MarkdownExec
2466
2692
  curr_document_filename: @delegate_object[:filename],
2467
2693
  inherited_block_names: ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
2468
2694
  inherited_dependencies: (link_state&.inherited_dependencies || {}).merge(dependencies || {}), ### merge, not replace, key data
2469
- inherited_lines: HashDelegator.code_merge(link_state&.inherited_lines, code_lines),
2695
+ inherited_lines: HashDelegator.code_merge(
2696
+ link_state&.inherited_lines, code_lines
2697
+ ),
2470
2698
  next_block_name: next_block_name,
2471
2699
  next_document_filename: next_document_filename,
2472
2700
  next_load_file: next_document_filename == @delegate_object[:filename] ? LoadFile::REUSE : LoadFile::LOAD
@@ -2487,14 +2715,29 @@ module MarkdownExec
2487
2715
  # @param selected [Hash] Selected item from the menu containing a YAML body.
2488
2716
  # @param tgt2 [Hash, nil] An optional target hash to update with YAML data.
2489
2717
  # @return [LoadFileLinkState] An instance indicating the next action for loading files.
2490
- def read_show_options_and_trigger_reuse(selected:, link_state: LinkState.new)
2718
+ def read_show_options_and_trigger_reuse(selected:,
2719
+ mdoc:, link_state: LinkState.new)
2491
2720
  obj = {}
2492
- data = YAML.load(selected[:body].join("\n"))
2493
- (data || []).each do |key, value|
2494
- sym_key = key.to_sym
2495
- obj[sym_key] = value
2496
2721
 
2497
- print_formatted_option(key, value) if @delegate_object[:menu_opts_set_format].present?
2722
+ # concatenated body of all required blocks loaded a YAML
2723
+ data = YAML.load(
2724
+ collect_required_code_lines(
2725
+ mdoc: mdoc, selected: selected,
2726
+ link_state: link_state, block_source: {}
2727
+ ).join("\n")
2728
+ ).transform_keys(&:to_sym)
2729
+
2730
+ if selected.shell == BlockType::OPTS
2731
+ obj = data
2732
+ else
2733
+ (data || []).each do |key, value|
2734
+ sym_key = key.to_sym
2735
+ obj[sym_key] = value
2736
+
2737
+ if @delegate_object[:menu_opts_set_format].present?
2738
+ print_formatted_option(key, value)
2739
+ end
2740
+ end
2498
2741
  end
2499
2742
 
2500
2743
  link_state.block_name = nil
@@ -2526,11 +2769,19 @@ module MarkdownExec
2526
2769
  resize_terminal
2527
2770
  end
2528
2771
 
2529
- opts[:console_height], opts[:console_width] = opts[:console_winsize] = IO.console.winsize if resized || !opts[:console_width]
2772
+ if resized || !opts[:console_width]
2773
+ opts[:console_height], opts[:console_width] = opts[:console_winsize] =
2774
+ IO.console.winsize
2775
+ end
2530
2776
 
2531
- opts[:per_page] = opts[:select_page_height] = [opts[:console_height] - 3, 4].max unless opts[:select_page_height]&.positive?
2777
+ unless opts[:select_page_height]&.positive?
2778
+ opts[:per_page] =
2779
+ opts[:select_page_height] =
2780
+ [opts[:console_height] - 3, 4].max
2781
+ end
2532
2782
  rescue StandardError
2533
- HashDelegator.error_handler('register_console_attributes', { abort: true })
2783
+ HashDelegator.error_handler('register_console_attributes',
2784
+ { abort: true })
2534
2785
  end
2535
2786
 
2536
2787
  # Check if the delegate object responds to a given method.
@@ -2542,7 +2793,8 @@ module MarkdownExec
2542
2793
  true
2543
2794
  elsif @delegate_object.respond_to?(method_name, include_private)
2544
2795
  true
2545
- elsif method_name.to_s.end_with?('=') && @delegate_object.respond_to?(:[]=, include_private)
2796
+ elsif method_name.to_s.end_with?('=') && @delegate_object.respond_to?(:[]=,
2797
+ include_private)
2546
2798
  true
2547
2799
  else
2548
2800
  @delegate_object.respond_to?(method_name, include_private)
@@ -2593,7 +2845,8 @@ module MarkdownExec
2593
2845
  # input into path with wildcard for easy entry
2594
2846
  #
2595
2847
  case (name = prompt_select_code_filename(
2596
- [@delegate_object[:prompt_filespec_back], @delegate_object[:prompt_filespec_other]] + files,
2848
+ [@delegate_object[:prompt_filespec_back],
2849
+ @delegate_object[:prompt_filespec_other]] + files,
2597
2850
  string: @delegate_object[:prompt_select_code_file],
2598
2851
  color_sym: :prompt_color_after_script_execution
2599
2852
  ))
@@ -2613,37 +2866,46 @@ module MarkdownExec
2613
2866
  end
2614
2867
 
2615
2868
  # Presents a TTY prompt to select an option or exit, returns metadata including option and selected
2616
- def select_option_with_metadata(prompt_text, names, opts = {})
2869
+ def select_option_with_metadata(prompt_text, menu_items, opts = {})
2617
2870
  ## configure to environment
2618
2871
  #
2619
2872
  register_console_attributes(opts)
2620
2873
 
2621
2874
  # crashes if all menu options are disabled
2622
2875
  selection = @prompt.select(prompt_text,
2623
- names,
2876
+ menu_items,
2624
2877
  opts.merge(filter: true))
2625
- selected_name = names.find do |item|
2878
+
2879
+ selected = menu_items.find do |item|
2626
2880
  if item.instance_of?(Hash)
2627
- item[:dname] == selection
2881
+ (item[:name] || item[:dname]) == selection
2882
+ elsif item.instance_of?(MarkdownExec::FCB)
2883
+ item.dname == selection
2628
2884
  else
2629
2885
  item == selection
2630
2886
  end
2631
2887
  end
2632
- selected_name = { dname: selected_name } if selected_name.instance_of?(String)
2633
- unless selected_name
2634
- HashDelegator.error_handler('select_option_with_metadata', error: 'menu item not found')
2888
+ if selected.instance_of?(String)
2889
+ selected = FCB.new(dname: selected)
2890
+ elsif selected.instance_of?(Hash)
2891
+ selected = FCB.new(selected)
2892
+ end
2893
+ unless selected
2894
+ HashDelegator.error_handler('select_option_with_metadata',
2895
+ error: 'menu item not found')
2635
2896
  exit 1
2636
2897
  end
2637
2898
 
2638
- selected_name.merge(
2639
- if selection == menu_chrome_colored_option(:menu_option_back_name)
2640
- { option: selection, shell: BlockType::LINK }
2641
- elsif selection == menu_chrome_colored_option(:menu_option_exit_name)
2642
- { option: selection }
2643
- else
2644
- { selected: selection }
2645
- end
2646
- )
2899
+ if selection == menu_chrome_colored_option(:menu_option_back_name)
2900
+ selected.option = selection
2901
+ selected.shell = BlockType::LINK
2902
+ elsif selection == menu_chrome_colored_option(:menu_option_exit_name)
2903
+ selected.option = selection
2904
+ else
2905
+ selected.selected = selection
2906
+ end
2907
+
2908
+ selected
2647
2909
  rescue TTY::Reader::InputInterrupt
2648
2910
  exit 1
2649
2911
  rescue StandardError
@@ -2663,8 +2925,9 @@ module MarkdownExec
2663
2925
  block_name_from_cli ? @cli_block_name : link_state.block_name
2664
2926
  end
2665
2927
 
2666
- def set_delobj_menu_loop_vars(block_name_from_cli:, now_using_cli:, link_state:)
2667
- block_name_from_cli, now_using_cli = \
2928
+ def set_delobj_menu_loop_vars(block_name_from_cli:, now_using_cli:,
2929
+ link_state:)
2930
+ block_name_from_cli, now_using_cli =
2668
2931
  manage_cli_selection_state(block_name_from_cli: block_name_from_cli,
2669
2932
  now_using_cli: now_using_cli,
2670
2933
  link_state: link_state)
@@ -2681,7 +2944,7 @@ module MarkdownExec
2681
2944
 
2682
2945
  def set_environment_variables_for_block(selected)
2683
2946
  code_lines = []
2684
- YAML.load(selected[:body].join("\n"))&.each do |key, value|
2947
+ YAML.load(selected.body.join("\n"))&.each do |key, value|
2685
2948
  ENV[key] = value.to_s
2686
2949
 
2687
2950
  require 'shellwords'
@@ -2696,6 +2959,29 @@ module MarkdownExec
2696
2959
  code_lines
2697
2960
  end
2698
2961
 
2962
+ def shell_escape_asset_format(link_state)
2963
+ raw = @delegate_object[:saved_asset_format]
2964
+
2965
+ return raw unless @delegate_object[:shell_parameter_expansion]
2966
+
2967
+ # unchanged if no parameter expansion takes place
2968
+ return raw unless /$/ =~ raw
2969
+
2970
+ filespec = generate_temp_filename
2971
+ cmd = [@delegate_object[:shell], '-c', filespec].join(' ')
2972
+
2973
+ marker = Random.new.rand.to_s
2974
+
2975
+ code = (link_state&.inherited_lines || []) + ["echo -n \"#{marker}#{raw}\""]
2976
+ # &bt code
2977
+ File.write filespec, HashDelegator.join_code_lines(code)
2978
+ File.chmod 0o755, filespec
2979
+
2980
+ out = `#{cmd}`.sub(/.*?#{marker}/m, '')
2981
+ File.delete filespec
2982
+ out # &br
2983
+ end
2984
+
2699
2985
  def should_add_back_option?
2700
2986
  @delegate_object[:menu_with_back] && @link_history.prior_state_exist?
2701
2987
  end
@@ -2818,6 +3104,13 @@ module MarkdownExec
2818
3104
  end
2819
3105
  end
2820
3106
 
3107
+ ## apply options to current state
3108
+ #
3109
+ def update_menu_base(options)
3110
+ @menu_base_options.merge!(options)
3111
+ @delegate_object.merge!(options)
3112
+ end
3113
+
2821
3114
  def wait_for_stream_processing
2822
3115
  @process_mutex.synchronize do
2823
3116
  @process_cv.wait(@process_mutex)
@@ -2839,8 +3132,11 @@ module MarkdownExec
2839
3132
  @delegate_object[:prompt_select_block].to_s, :prompt_color_after_script_execution
2840
3133
  )
2841
3134
 
2842
- block_menu = prepare_blocks_menu(menu_blocks)
2843
- return SelectedBlockMenuState.new(nil, MenuState::EXIT) if block_menu.empty?
3135
+ menu_items = prepare_blocks_menu(menu_blocks)
3136
+ if menu_items.empty?
3137
+ return SelectedBlockMenuState.new(nil, OpenStruct.new,
3138
+ MenuState::EXIT)
3139
+ end
2844
3140
 
2845
3141
  # default value may not match if color is different from originating menu (opts changed while processing)
2846
3142
  selection_opts = if default && menu_blocks.map(&:dname).include?(default)
@@ -2852,7 +3148,7 @@ module MarkdownExec
2852
3148
  sph = @delegate_object[:select_page_height]
2853
3149
  selection_opts.merge!(per_page: sph)
2854
3150
 
2855
- selected_option = select_option_with_metadata(prompt_title, block_menu,
3151
+ selected_option = select_option_with_metadata(prompt_title, menu_items,
2856
3152
  selection_opts)
2857
3153
  determine_block_state(selected_option)
2858
3154
  end
@@ -2867,7 +3163,7 @@ module MarkdownExec
2867
3163
  exts: '.sh',
2868
3164
  filename: @delegate_object[:filename],
2869
3165
  prefix: @delegate_object[:saved_script_filename_prefix],
2870
- saved_asset_format: @delegate_object[:saved_asset_format],
3166
+ saved_asset_format: shell_escape_asset_format(@dml_link_state),
2871
3167
  time: time_now).generate_name
2872
3168
  @run_state.saved_filespec =
2873
3169
  File.join(@delegate_object[:saved_script_folder],
@@ -2918,7 +3214,8 @@ module MarkdownExec
2918
3214
  save_expr = link_block_data.fetch(LinkKeys::SAVE, '')
2919
3215
  if save_expr.present?
2920
3216
  save_filespec = save_filespec_from_expression(save_expr)
2921
- File.write(save_filespec, HashDelegator.join_code_lines(link_state&.inherited_lines))
3217
+ File.write(save_filespec,
3218
+ HashDelegator.join_code_lines(link_state&.inherited_lines))
2922
3219
  @delegate_object[:filename]
2923
3220
  else
2924
3221
  link_block_data[LinkKeys::FILE] || @delegate_object[:filename]
@@ -2950,7 +3247,11 @@ module MarkdownExec
2950
3247
  obj[key] = cleaned_value if value.is_a?(Hash) || value.is_a?(Struct)
2951
3248
  end
2952
3249
 
2953
- obj.reject! { |_key, value| [nil, '', [], {}, nil].include?(value) } if obj.is_a?(Hash)
3250
+ if obj.is_a?(Hash)
3251
+ obj.reject! do |_key, value|
3252
+ [nil, '', [], {}, nil].include?(value)
3253
+ end
3254
+ end
2954
3255
 
2955
3256
  obj
2956
3257
  end
@@ -3014,7 +3315,8 @@ module MarkdownExec
3014
3315
 
3015
3316
  # Test case for empty body
3016
3317
  def test_next_link_state
3017
- @hd.next_link_state(block_name_from_cli: nil, was_using_cli: nil, block_state: nil, block_name: nil)
3318
+ @hd.next_link_state(block_name_from_cli: nil, was_using_cli: nil, block_state: nil,
3319
+ block_name: nil)
3018
3320
  end
3019
3321
  end
3020
3322
 
@@ -3061,15 +3363,18 @@ module MarkdownExec
3061
3363
  # Test case for non-empty body with 'file' key
3062
3364
  def test_push_link_history_and_trigger_load_with_file_key
3063
3365
  body = ["file: sample_file\nblock: sample_block\nvars:\n KEY: VALUE"]
3064
- expected_result = LoadFileLinkState.new(LoadFile::LOAD,
3065
- LinkState.new(block_name: 'sample_block',
3066
- document_filename: 'sample_file',
3067
- inherited_dependencies: {},
3068
- inherited_lines: ['# ', 'KEY="VALUE"']))
3366
+ expected_result = LoadFileLinkState.new(
3367
+ LoadFile::LOAD,
3368
+ LinkState.new(block_name: 'sample_block',
3369
+ document_filename: 'sample_file',
3370
+ inherited_dependencies: {},
3371
+ inherited_lines: ['# ', 'KEY="VALUE"'])
3372
+ )
3069
3373
  assert_equal expected_result,
3070
3374
  @hd.push_link_history_and_trigger_load(
3071
3375
  link_block_body: body,
3072
- selected: FCB.new(block_name: 'sample_block', filename: 'sample_file')
3376
+ selected: FCB.new(block_name: 'sample_block',
3377
+ filename: 'sample_file')
3073
3378
  )
3074
3379
  end
3075
3380
 
@@ -3178,20 +3483,20 @@ module MarkdownExec
3178
3483
  end
3179
3484
 
3180
3485
  def test_block_find_with_match
3181
- blocks = [{ key: 'value1' }, { key: 'value2' }]
3182
- result = HashDelegator.block_find(blocks, :key, 'value1')
3183
- assert_equal({ key: 'value1' }, result)
3486
+ blocks = [FCB.new(text: 'value1'), FCB.new(text: 'value2')]
3487
+ result = HashDelegator.block_find(blocks, :text, 'value1')
3488
+ assert_equal('value1', result.text)
3184
3489
  end
3185
3490
 
3186
3491
  def test_block_find_without_match
3187
- blocks = [{ key: 'value1' }, { key: 'value2' }]
3188
- result = HashDelegator.block_find(blocks, :key, 'value3')
3492
+ blocks = [FCB.new(text: 'value1'), FCB.new(text: 'value2')]
3493
+ result = HashDelegator.block_find(blocks, :text, 'missing_value')
3189
3494
  assert_nil result
3190
3495
  end
3191
3496
 
3192
3497
  def test_block_find_with_default
3193
- blocks = [{ key: 'value1' }, { key: 'value2' }]
3194
- result = HashDelegator.block_find(blocks, :key, 'value3', 'default')
3498
+ blocks = [FCB.new(text: 'value1'), FCB.new(text: 'value2')]
3499
+ result = HashDelegator.block_find(blocks, :text, 'missing_value', 'default')
3195
3500
  assert_equal 'default', result
3196
3501
  end
3197
3502
  end
@@ -3227,7 +3532,7 @@ module MarkdownExec
3227
3532
  @hd = HashDelegator.new
3228
3533
  @hd.instance_variable_set(:@delegate_object, {})
3229
3534
  @mdoc = mock('YourMDocClass')
3230
- @selected = { shell: BlockType::VARS, body: ['key: value'] }
3535
+ @selected = FCB.new(shell: BlockType::VARS, body: ['key: value'])
3231
3536
  HashDelegator.stubs(:read_required_blocks_from_temp_file).returns([])
3232
3537
  @hd.stubs(:string_send_color)
3233
3538
  @hd.stubs(:print)
@@ -3236,7 +3541,8 @@ module MarkdownExec
3236
3541
  def test_collect_required_code_lines_with_vars
3237
3542
  YAML.stubs(:load).returns({ 'key' => 'value' })
3238
3543
  @mdoc.stubs(:collect_recursively_required_code).returns({ code: ['code line'] })
3239
- result = @hd.collect_required_code_lines(mdoc: @mdoc, selected: @selected, block_source: {})
3544
+ result = @hd.collect_required_code_lines(mdoc: @mdoc, selected: @selected,
3545
+ block_source: {})
3240
3546
 
3241
3547
  assert_equal ['code line', 'key="value"'], result
3242
3548
  end
@@ -3257,18 +3563,24 @@ module MarkdownExec
3257
3563
 
3258
3564
  result = @hd.load_cli_or_user_selected_block(all_blocks: all_blocks)
3259
3565
 
3260
- assert_equal all_blocks.first.merge(block_name_from_ui: false), result.block
3566
+ assert_equal all_blocks.first,
3567
+ result.block
3568
+ assert_equal OpenStruct.new(block_name_from_ui: false),
3569
+ result.source
3261
3570
  assert_nil result.state
3262
3571
  end
3263
3572
 
3264
3573
  def test_user_selected_block
3265
- block_state = SelectedBlockMenuState.new({ oname: 'block2' },
3574
+ block_state = SelectedBlockMenuState.new({ oname: 'block2' }, OpenStruct.new,
3266
3575
  :some_state)
3267
3576
  @hd.stubs(:wait_for_user_selected_block).returns(block_state)
3268
3577
 
3269
3578
  result = @hd.load_cli_or_user_selected_block
3270
3579
 
3271
- assert_equal block_state.block.merge(block_name_from_ui: true), result.block
3580
+ assert_equal block_state.block,
3581
+ result.block
3582
+ assert_equal OpenStruct.new(block_name_from_ui: true),
3583
+ result.source
3272
3584
  assert_equal :some_state, result.state
3273
3585
  end
3274
3586
  end
@@ -3351,7 +3663,7 @@ module MarkdownExec
3351
3663
  end
3352
3664
 
3353
3665
  def test_determine_block_state_exit
3354
- selected_option = { oname: 'Formatted Option' }
3666
+ selected_option = FCB.new(oname: 'Formatted Option')
3355
3667
  @hd.stubs(:menu_chrome_formatted_option).with(:menu_option_exit_name).returns('Formatted Option')
3356
3668
 
3357
3669
  result = @hd.determine_block_state(selected_option)
@@ -3361,7 +3673,7 @@ module MarkdownExec
3361
3673
  end
3362
3674
 
3363
3675
  def test_determine_block_state_back
3364
- selected_option = { oname: 'Formatted Back Option' }
3676
+ selected_option = FCB.new(oname: 'Formatted Back Option')
3365
3677
  @hd.stubs(:menu_chrome_formatted_option).with(:menu_option_back_name).returns('Formatted Back Option')
3366
3678
  result = @hd.determine_block_state(selected_option)
3367
3679
 
@@ -3370,7 +3682,7 @@ module MarkdownExec
3370
3682
  end
3371
3683
 
3372
3684
  def test_determine_block_state_continue
3373
- selected_option = { oname: 'Other Option' }
3685
+ selected_option = FCB.new(oname: 'Other Option')
3374
3686
 
3375
3687
  result = @hd.determine_block_state(selected_option)
3376
3688
 
@@ -3480,7 +3792,8 @@ module MarkdownExec
3480
3792
  def test_format_execution_stream_with_empty_key
3481
3793
  @hd.instance_variable_get(:@run_state).stubs(:files).returns({})
3482
3794
 
3483
- result = HashDelegator.format_execution_stream(nil, ExecutionStreams::STD_ERR)
3795
+ result = HashDelegator.format_execution_stream(nil,
3796
+ ExecutionStreams::STD_ERR)
3484
3797
 
3485
3798
  assert_equal '', result
3486
3799
  end
@@ -3639,7 +3952,8 @@ module MarkdownExec
3639
3952
  end
3640
3953
 
3641
3954
  def test_iter_blocks_from_nested_files
3642
- @hd.cfile.expect(:readlines, ['line 1', 'line 2'], ['test.md'], import_paths: nil)
3955
+ @hd.cfile.expect(:readlines, ['line 1', 'line 2'], ['test.md'],
3956
+ import_paths: nil)
3643
3957
  selected_messages = ['filtered message']
3644
3958
 
3645
3959
  result = @hd.iter_blocks_from_nested_files { selected_messages }
@@ -3745,7 +4059,8 @@ module MarkdownExec
3745
4059
 
3746
4060
  def test_yield_line_if_selected_with_line
3747
4061
  block_called = false
3748
- HashDelegator.yield_line_if_selected('Test line', [:line]) do |type, content|
4062
+ HashDelegator.yield_line_if_selected('Test line',
4063
+ [:line]) do |type, content|
3749
4064
  block_called = true
3750
4065
  assert_equal :line, type
3751
4066
  assert_equal 'Test line', content.body[0]
@@ -3780,15 +4095,18 @@ module MarkdownExec
3780
4095
  def test_update_menu_attrib_yield_selected_with_body
3781
4096
  HashDelegator.expects(:initialize_fcb_names).with(@fcb)
3782
4097
  HashDelegator.expects(:default_block_title_from_body).with(@fcb)
3783
- Filter.expects(:yield_to_block_if_applicable).with(@fcb, [:some_message], {})
4098
+ Filter.expects(:yield_to_block_if_applicable).with(@fcb, [:some_message],
4099
+ {})
3784
4100
 
3785
- HashDelegator.update_menu_attrib_yield_selected(fcb: @fcb, messages: [:some_message])
4101
+ HashDelegator.update_menu_attrib_yield_selected(fcb: @fcb,
4102
+ messages: [:some_message])
3786
4103
  end
3787
4104
 
3788
4105
  def test_update_menu_attrib_yield_selected_without_body
3789
4106
  @fcb.stubs(:body).returns(nil)
3790
4107
  HashDelegator.expects(:initialize_fcb_names).with(@fcb)
3791
- HashDelegator.update_menu_attrib_yield_selected(fcb: @fcb, messages: [:some_message])
4108
+ HashDelegator.update_menu_attrib_yield_selected(fcb: @fcb,
4109
+ messages: [:some_message])
3792
4110
  end
3793
4111
  end
3794
4112
 
@@ -3864,28 +4182,35 @@ module MarkdownExec
3864
4182
  def test_absolute_path_returns_unchanged
3865
4183
  absolute_path = '/usr/local/bin'
3866
4184
  expression = 'path/to/*/directory'
3867
- assert_equal absolute_path, PathUtils.resolve_path_or_substitute(absolute_path, expression)
4185
+ assert_equal absolute_path,
4186
+ PathUtils.resolve_path_or_substitute(absolute_path,
4187
+ expression)
3868
4188
  end
3869
4189
 
3870
4190
  def test_relative_path_gets_substituted
3871
4191
  relative_path = 'my_folder'
3872
4192
  expression = 'path/to/*/directory'
3873
4193
  expected_output = 'path/to/my_folder/directory'
3874
- assert_equal expected_output, PathUtils.resolve_path_or_substitute(relative_path, expression)
4194
+ assert_equal expected_output,
4195
+ PathUtils.resolve_path_or_substitute(relative_path,
4196
+ expression)
3875
4197
  end
3876
4198
 
3877
4199
  def test_path_with_no_slash_substitutes_correctly
3878
4200
  relative_path = 'data'
3879
4201
  expression = 'path/to/*/directory'
3880
4202
  expected_output = 'path/to/data/directory'
3881
- assert_equal expected_output, PathUtils.resolve_path_or_substitute(relative_path, expression)
4203
+ assert_equal expected_output,
4204
+ PathUtils.resolve_path_or_substitute(relative_path,
4205
+ expression)
3882
4206
  end
3883
4207
 
3884
4208
  def test_empty_path_substitution
3885
4209
  empty_path = ''
3886
4210
  expression = 'path/to/*/directory'
3887
4211
  expected_output = 'path/to//directory'
3888
- assert_equal expected_output, PathUtils.resolve_path_or_substitute(empty_path, expression)
4212
+ assert_equal expected_output,
4213
+ PathUtils.resolve_path_or_substitute(empty_path, expression)
3889
4214
  end
3890
4215
 
3891
4216
  # Test formatting a string containing UTF-8 characters
@@ -3934,7 +4259,8 @@ module MarkdownExec
3934
4259
  private
3935
4260
 
3936
4261
  def prompt_for_filespec_with_wildcard(filespec)
3937
- puts format(@delegate_object[:prompt_show_expr_format], { expr: filespec })
4262
+ puts format(@delegate_object[:prompt_show_expr_format],
4263
+ { expr: filespec })
3938
4264
  puts @delegate_object[:prompt_enter_filespec]
3939
4265
 
3940
4266
  begin