markdown_exec 2.1.0 → 2.2.0

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.
@@ -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