markdown_exec 2.6.0 → 2.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -2
  3. data/Gemfile.lock +2 -2
  4. data/Rakefile +36 -0
  5. data/bats/command-substitution.bats +8 -0
  6. data/bats/options.bats +4 -4
  7. data/bats/plain.bats +8 -0
  8. data/bats/table.bats +6 -1
  9. data/bats/variable-expansion.bats +4 -4
  10. data/bin/bmde +46 -2
  11. data/bin/tab_completion.sh +1 -1
  12. data/docs/dev/command-substitution.md +24 -0
  13. data/docs/dev/load_code.md +14 -0
  14. data/docs/dev/no-active-elements.md +6 -0
  15. data/docs/dev/screen-width.md +21 -0
  16. data/docs/dev/table-invalid.md +20 -0
  17. data/examples/bash-blocks.md +2 -1
  18. data/examples/block-names.md +2 -1
  19. data/examples/block-types.md +2 -1
  20. data/examples/data-files.md +1 -0
  21. data/examples/document_options.md +1 -0
  22. data/examples/example-document-opts.md +6 -0
  23. data/examples/index.md +1 -1
  24. data/examples/interrupt.md +1 -0
  25. data/examples/link-blocks-load-save.md +8 -10
  26. data/examples/link-blocks-vars.md +12 -9
  27. data/examples/linked.md +9 -12
  28. data/examples/linked1.md +7 -6
  29. data/examples/linked2.md +6 -7
  30. data/examples/linked3.md +6 -5
  31. data/examples/linked_show.md +5 -4
  32. data/examples/nickname.md +1 -0
  33. data/examples/opts-blocks-require.md +1 -0
  34. data/examples/opts-blocks.md +1 -0
  35. data/examples/opts_output_execution.md +1 -0
  36. data/examples/pass-through-arguments.md +1 -0
  37. data/examples/pause-after-execution.md +1 -0
  38. data/examples/port-blocks.md +14 -10
  39. data/examples/save.md +1 -0
  40. data/examples/text-markup.md +2 -1
  41. data/examples/variable-expansion-save-block.md +48 -0
  42. data/examples/variable-expansion.md +3 -1
  43. data/examples/vars-blocks.md +14 -24
  44. data/examples/wrap.md +2 -1
  45. data/lib/collapser.rb +83 -47
  46. data/lib/evaluate_shell_expressions.rb +3 -2
  47. data/lib/fcb.rb +8 -2
  48. data/lib/hash_delegator.rb +214 -118
  49. data/lib/input_sequencer.rb +0 -7
  50. data/lib/markdown_exec/version.rb +1 -1
  51. data/lib/markdown_exec.rb +6 -1
  52. data/lib/menu.src.yml +22 -9
  53. data/lib/menu.yml +22 -7
  54. data/lib/ww.rb +1 -1
  55. metadata +16 -9
  56. data/docs/dev/table-crash.md +0 -39
  57. data/examples/load_code.md +0 -10
  58. /data/{examples → docs/dev}/load1.sh +0 -0
@@ -264,10 +264,9 @@ module HashDelegatorSelf
264
264
  end
265
265
 
266
266
  # find tables in multiple lines and format horizontally
267
- def tables_into_columns!(blocks_menu, delegate_object)
267
+ def tables_into_columns!(blocks_menu, delegate_object, screen_width_for_table)
268
268
  return unless delegate_object[:tables_into_columns]
269
269
 
270
- screenwidth = delegate_object[:console_width]
271
270
  lines = blocks_menu.map(&:oname)
272
271
  text_tables = TableExtractor.extract_tables(
273
272
  lines,
@@ -301,10 +300,10 @@ module HashDelegatorSelf
301
300
  # replace text in each block
302
301
  range.each.with_index do |block_ind, ind|
303
302
  fcb = blocks_menu[block_ind]
304
- fcb.s3formatted_table_row = fcb.padded = table__hs[ind] ####
303
+ fcb.s3formatted_table_row = fcb.padded = table__hs[ind]
305
304
  fcb.padded_width = table__hs[ind].padded_width
306
305
  if fcb.center
307
- cw = (screenwidth - table__hs[ind].padded_width) / 2
306
+ cw = (screen_width_for_table - table__hs[ind].padded_width) / 2
308
307
  if cw.positive?
309
308
  indent = ' ' * cw
310
309
  fcb.s3indent = fcb.indent = indent
@@ -321,7 +320,7 @@ module HashDelegatorSelf
321
320
  # s0printable: line_obj[:text],
322
321
  # s1decorated: decorated,
323
322
  # s2title = fcb.oname
324
- # s3formatted_table_row = fcb.padded = table__hs[ind]####
323
+ # s3formatted_table_row = fcb.padded = table__hs[ind]
325
324
 
326
325
  # Creates a TTY prompt with custom settings. Specifically,
327
326
  # it disables the default 'cross' symbol and
@@ -642,8 +641,6 @@ module MarkdownExec
642
641
  position: :final)
643
642
  end
644
643
 
645
- public
646
-
647
644
  # Appends a chrome block, which is a menu option for Back or Exit
648
645
  #
649
646
  # @param all_blocks [Array] The current blocks in the menu
@@ -776,7 +773,7 @@ module MarkdownExec
776
773
  end
777
774
 
778
775
  def assign_key_value_in_bash(key, value)
779
- if value =~ /["$\\`]/
776
+ if value.to_s =~ /["$\\`]/
780
777
  # requiring ShellWords to write into Bash scripts
781
778
  "#{key}=#{Shellwords.escape(value)}"
782
779
  else
@@ -791,7 +788,9 @@ module MarkdownExec
791
788
  # The method categorizes blocks based on their type and processes them accordingly.
792
789
  #
793
790
  # @return [Array<FCB>] An array of FCB objects representing the blocks.
794
- def blocks_from_nested_files
791
+ def blocks_from_nested_files(
792
+ link_state: @dml_link_state || LinkState.new
793
+ )
795
794
  register_console_attributes(@delegate_object)
796
795
  @decor_patterns_from_delegate_object_for_block_create = collect_line_decor_patterns(@delegate_object)
797
796
 
@@ -799,6 +798,23 @@ module MarkdownExec
799
798
  blocks = []
800
799
  iter_blocks_from_nested_files do |btype, fcb|
801
800
  count += 1
801
+
802
+ # text substitution in menu
803
+ #
804
+ expand_references = lambda do |fcb|
805
+ expand_variable_references!(blocks: [fcb], link_state: link_state,
806
+ initial_code_required: false)
807
+ expand_variable_references!(
808
+ blocks: [fcb],
809
+ echo_format: '%s',
810
+ initial_code_required: false,
811
+ key_format: '$(%s)',
812
+ link_state: link_state,
813
+ group_name: :command,
814
+ pattern: options_command_substitution_regexp
815
+ )
816
+ end
817
+
802
818
  case btype
803
819
  when :blocks
804
820
  if @delegate_object[:bash]
@@ -806,16 +822,18 @@ module MarkdownExec
806
822
  block_calls_scan: @delegate_object[:block_calls_scan],
807
823
  block_name_match: @delegate_object[:block_name_match],
808
824
  block_name_nick_match: @delegate_object[:block_name_nick_match],
809
- id: "*#{count}",
825
+ id: "*#{count}"
810
826
  ) do |oname, color|
811
827
  apply_block_type_color_option(oname, color)
812
828
  end
813
829
  end
830
+ expand_references.call(fcb)
814
831
  blocks << fcb
815
832
  when :filter # types accepted
816
833
  %i[blocks line]
817
834
  when :line
818
835
  unless @delegate_object[:no_chrome]
836
+ expand_references.call(fcb)
819
837
  create_and_add_chrome_blocks(blocks, fcb, id: "*#{count}",
820
838
  init_ids: init_ids)
821
839
  end
@@ -853,10 +871,12 @@ module MarkdownExec
853
871
 
854
872
  # private
855
873
 
856
- def build_replacement_dictionary(commands, link_state)
874
+ def build_replacement_dictionary(commands, link_state,
875
+ initial_code_required: false, key_format:)
857
876
  evaluate_shell_expressions(
858
- link_state.inherited_lines_block, commands,
859
- key_format: "${%s}" # no need to escape variable name for regexp
877
+ (link_state&.inherited_lines_block || ''), commands,
878
+ initial_code_required: initial_code_required,
879
+ key_format: key_format
860
880
  ) # !!t
861
881
  end
862
882
 
@@ -1022,9 +1042,7 @@ module MarkdownExec
1022
1042
  end
1023
1043
 
1024
1044
  def command_execute_in_own_window_format_arguments(
1025
- home: Dir.pwd,
1026
- erls:,
1027
- rest: ''
1045
+ erls:, home: Dir.pwd, rest: ''
1028
1046
  )
1029
1047
  {
1030
1048
  batch_index: @run_state.batch_index,
@@ -1145,19 +1163,21 @@ module MarkdownExec
1145
1163
  end
1146
1164
 
1147
1165
  def count_named_group_occurrences(
1148
- blocks, pattern, exclude_types: [BlockType::SHELL]
1166
+ blocks, pattern, exclude_types: [BlockType::SHELL],
1167
+ group_name:
1149
1168
  )
1150
1169
  # Initialize a counter for named group occurrences
1151
1170
  occurrence_count = Hash.new(0)
1171
+ return occurrence_count if pattern == //
1152
1172
 
1153
1173
  blocks.each do |block|
1154
1174
  # Skip processing for shell-type blocks
1155
1175
  next if exclude_types.include?(block.type)
1156
1176
 
1157
1177
  # Scan each block name for matches of the pattern
1158
- block.oname.scan(pattern) do |(_, variable_name)|
1178
+ ([block.oname || ''] + block.body).join("\n").scan(pattern) do |(_, _variable_name)|
1159
1179
  pattern.match($LAST_MATCH_INFO.to_s) # Reapply match for named groups
1160
- occurrence_count[$LAST_MATCH_INFO[:variable]] += 1
1180
+ occurrence_count[$LAST_MATCH_INFO[group_name]] += 1
1161
1181
  end
1162
1182
  end
1163
1183
 
@@ -1175,18 +1195,14 @@ module MarkdownExec
1175
1195
  # @param color_method [Symbol] The color method to apply
1176
1196
  # to the block's display name.
1177
1197
  # return number of lines added
1178
- def create_and_add_chrome_block(blocks:, match_data:,
1179
- collapse: nil,
1180
- format_option:, color_method:,
1181
- case_conversion: nil,
1182
- center: nil,
1183
- decor_patterns: [],
1184
- disabled: true,
1185
- id: '',
1186
- level: 0,
1187
- type: '',
1188
- wrap: nil)
1189
- line_cap = NamedCaptureExtractor::extract_named_group2(match_data)
1198
+ def create_and_add_chrome_block(
1199
+ blocks:, case_conversion: nil, center: nil,
1200
+ collapse: nil, color_method:, decor_patterns: [],
1201
+ disabled: true, format_option:, id: '',
1202
+ level: 0, match_data:, type: '',
1203
+ wrap: nil
1204
+ )
1205
+ line_cap = NamedCaptureExtractor.extract_named_group2(match_data)
1190
1206
  # replace tabs in indent
1191
1207
  line_cap[:indent] ||= ''
1192
1208
  line_cap[:indent] = line_cap[:indent].dup if line_cap[:indent].frozen?
@@ -1199,11 +1215,9 @@ module MarkdownExec
1199
1215
  line_cap[:collapse] ||= ''
1200
1216
  line_cap[:line] ||= ''
1201
1217
 
1202
- accepted_width = @delegate_object[:console_width] - 2
1203
-
1204
1218
  line_caps = [line_cap]
1205
- if wrap && line_cap[:text].length > accepted_width
1206
- wrapper = StringWrapper.new(width: accepted_width - line_cap[:indent].length)
1219
+ if wrap && line_cap[:text].length > screen_width_for_wrapping
1220
+ wrapper = StringWrapper.new(width: screen_width_for_wrapping - line_cap[:indent].length)
1207
1221
  line_caps = wrapper.wrap(line_cap[:text]).map do |wrapped_line|
1208
1222
  line_cap.dup.merge(text: wrapped_line)
1209
1223
  end
@@ -1212,8 +1226,8 @@ module MarkdownExec
1212
1226
  if center
1213
1227
  line_caps.each do |line_obj|
1214
1228
  line_obj[:indent] =
1215
- if line_obj[:text].length < accepted_width
1216
- ' ' * ((accepted_width - line_obj[:text].length) / 2)
1229
+ if line_obj[:text].length < screen_width_for_wrapping
1230
+ ' ' * ((screen_width_for_wrapping - line_obj[:text].length) / 2)
1217
1231
  else
1218
1232
  ''
1219
1233
  end
@@ -1248,8 +1262,6 @@ module MarkdownExec
1248
1262
  collapse: collapse.nil? ? (line_obj[:collapse] == COLLAPSIBLE_TOKEN_COLLAPSE) : collapse,
1249
1263
  token: line_obj[:collapse],
1250
1264
  disabled: disabled ? TtyMenu::DISABLE : nil,
1251
- ####
1252
- # id: "#{@delegate_object[:filename]}:#{index}",
1253
1265
  id: "#{id}.#{index}",
1254
1266
  level: level,
1255
1267
  s0indent: indent,
@@ -1292,7 +1304,7 @@ module MarkdownExec
1292
1304
  when COLLAPSIBLE_TOKEN_EXPAND
1293
1305
  false
1294
1306
  else
1295
- false####
1307
+ false
1296
1308
  end,
1297
1309
 
1298
1310
  color_method: criteria[:color] &&
@@ -1864,6 +1876,7 @@ module MarkdownExec
1864
1876
  exit_prompt: @delegate_object[:prompt_filespec_back],
1865
1877
  filename_pattern: @delegate_object[:vars_block_filename_pattern],
1866
1878
  glob: @delegate_object[:document_configurations_glob],
1879
+ menu_options: HashDelegator.options_for_tty_menu(@delegate_object),
1867
1880
  view: @delegate_object[:vars_block_filename_view]
1868
1881
  )
1869
1882
  block_data = HashDelegator.parse_yaml_data_from_body(selected.body)
@@ -1878,7 +1891,7 @@ module MarkdownExec
1878
1891
  ).sort.map do |file|
1879
1892
  { name: format(
1880
1893
  block_data['view'] || view,
1881
- NamedCaptureExtractor::extract_named_group2(
1894
+ NamedCaptureExtractor.extract_named_group2(
1882
1895
  file.match(
1883
1896
  Regexp.new(block_data['filename_pattern'] ||
1884
1897
  filename_pattern)
@@ -1887,13 +1900,15 @@ module MarkdownExec
1887
1900
  ),
1888
1901
  oname: file }
1889
1902
  end,
1890
- simple_menu_options
1903
+ menu_options.merge(
1904
+ cycle: true
1905
+ )
1891
1906
  )
1892
1907
  if selected_option.dname != exit_prompt
1893
1908
  File.readlines(selected_option.oname, chomp: true)
1894
1909
  end
1895
1910
  else
1896
- warn "No matching files found" ###
1911
+ warn 'No matching files found'
1897
1912
  end
1898
1913
  end
1899
1914
 
@@ -1964,7 +1979,7 @@ module MarkdownExec
1964
1979
  end
1965
1980
 
1966
1981
  save_filespec_from_expression(directory_glob).tap do |save_filespec|
1967
- if save_filespec && save != exit_prompt
1982
+ if save_filespec && save_filespec != exit_prompt
1968
1983
  begin
1969
1984
  File.write(save_filespec,
1970
1985
  HashDelegator.join_code_lines(code_lines))
@@ -2140,7 +2155,9 @@ module MarkdownExec
2140
2155
  )
2141
2156
  # update blocks
2142
2157
  #
2143
- Regexp.union(replacements.keys).tap do |pattern|
2158
+ Regexp.union(replacements.keys.map do |word|
2159
+ Regexp.new(Regexp.escape(word))
2160
+ end).tap do |pattern|
2144
2161
  menu_blocks.each do |block|
2145
2162
  next if exclude_types.include?(block.type)
2146
2163
 
@@ -2150,25 +2167,31 @@ module MarkdownExec
2150
2167
  end
2151
2168
 
2152
2169
  def expand_variable_references!(
2153
- # echo_format: 'echo "$%s"',
2154
2170
  echo_format: 'echo $%s',
2155
2171
  link_state:,
2156
2172
  blocks:,
2157
- pattern: Regexp.new(@delegate_object[:variable_expression_regexp])
2173
+ group_name: :variable,
2174
+ initial_code_required: false,
2175
+ key_format: '${%s}',
2176
+ pattern: nil
2158
2177
  )
2159
- # Count occurrences of named groups in each block
2160
- variable_counts = count_named_group_occurrences(blocks, pattern)
2178
+ pattern ||= options_variable_expression_regexp
2179
+ return if pattern.nil?
2180
+
2181
+ variable_counts = count_named_group_occurrences(blocks, pattern,
2182
+ group_name: group_name)
2183
+ return if variable_counts.nil?
2161
2184
 
2162
- # Generate echo commands for each variable based on its count
2163
2185
  echo_commands = generate_echo_commands(variable_counts, echo_format)
2164
2186
 
2165
- # Build a dictionary to replace variables with the corresponding commands
2166
- replacements = build_replacement_dictionary(echo_commands, link_state)
2187
+ replacements = build_replacement_dictionary(
2188
+ echo_commands, link_state,
2189
+ initial_code_required: initial_code_required,
2190
+ key_format: key_format
2191
+ )
2167
2192
 
2168
- # Exit early if no replacements are needed
2169
2193
  return if replacements.nil?
2170
2194
 
2171
- # Expand each block with replacements from the dictionary
2172
2195
  expand_blocks_with_replacements(blocks, replacements)
2173
2196
  end
2174
2197
 
@@ -2284,7 +2307,7 @@ module MarkdownExec
2284
2307
  # commands to echo variables
2285
2308
  #
2286
2309
  commands = {}
2287
- variable_counts.each do |variable, count|
2310
+ variable_counts.each do |variable, _count|
2288
2311
  command = format(echo_format, variable)
2289
2312
  commands[variable] = command
2290
2313
  end
@@ -2380,8 +2403,6 @@ module MarkdownExec
2380
2403
  }
2381
2404
  end
2382
2405
 
2383
- public
2384
-
2385
2406
  # Iterates through blocks in a file, applying the provided block to each line.
2386
2407
  # The iteration only occurs if the file exists.
2387
2408
  # @yield [Symbol] :filter Yields to obtain selected messages for processing.
@@ -2392,15 +2413,15 @@ module MarkdownExec
2392
2413
  selected_types = yield :filter
2393
2414
  cfile.readlines(
2394
2415
  @delegate_object[:filename],
2395
- import_paths: @delegate_object[:import_paths]&.split(':')
2416
+ import_paths: options_import_paths
2396
2417
  ).each_with_index do |nested_line, index|
2397
- if nested_line
2398
- update_line_and_block_state(
2399
- nested_line, state, selected_types,
2400
- id: "#{@delegate_object[:filename]}:#{index}",
2401
- &block
2402
- )
2403
- end
2418
+ next unless nested_line
2419
+
2420
+ update_line_and_block_state(
2421
+ nested_line, state, selected_types,
2422
+ id: "#{@delegate_object[:filename]}:#{index}",
2423
+ &block
2424
+ )
2404
2425
  end
2405
2426
  end
2406
2427
 
@@ -2580,7 +2601,7 @@ module MarkdownExec
2580
2601
  end
2581
2602
 
2582
2603
  def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [],
2583
- default: nil)
2604
+ prior_answer: nil)
2584
2605
  if @delegate_object[:block_name].present?
2585
2606
  block = all_blocks.find do |item|
2586
2607
  item.pub_name == @delegate_object[:block_name]
@@ -2588,7 +2609,7 @@ module MarkdownExec
2588
2609
  source = OpenStruct.new(block_name_from_ui: false)
2589
2610
  else
2590
2611
  block_state = wait_for_user_selected_block(all_blocks, menu_blocks,
2591
- default)
2612
+ prior_answer)
2592
2613
  return if block_state.nil?
2593
2614
 
2594
2615
  block = block_state.block
@@ -2693,11 +2714,6 @@ module MarkdownExec
2693
2714
  @delegate_object.merge!(compressed_ids: @compressed_ids)
2694
2715
  )
2695
2716
 
2696
- # text substitution in menu
2697
- #
2698
- expand_variable_references!(blocks: menu_blocks, link_state: link_state)
2699
- # expand_command_substition!(blocks: menu_blocks, link_state: link_state)
2700
-
2701
2717
  # chrome for menu
2702
2718
  #
2703
2719
  add_menu_chrome_blocks!(id: id, menu_blocks: menu_blocks,
@@ -2705,7 +2721,8 @@ module MarkdownExec
2705
2721
 
2706
2722
  ### compress empty lines
2707
2723
  HashDelegator.delete_consecutive_blank_lines!(menu_blocks)
2708
- HashDelegator.tables_into_columns!(menu_blocks, @delegate_object) ####
2724
+ HashDelegator.tables_into_columns!(menu_blocks, @delegate_object,
2725
+ screen_width_for_table)
2709
2726
 
2710
2727
  [all_blocks, menu_blocks, mdoc]
2711
2728
  end
@@ -2805,6 +2822,19 @@ module MarkdownExec
2805
2822
  )
2806
2823
  end
2807
2824
 
2825
+ def options_command_substitution_regexp
2826
+ Regexp.new(@delegate_object[:command_substitution_regexp] || '')
2827
+ end
2828
+
2829
+ def options_import_paths
2830
+ @delegate_object[:import_paths]&.split(':') || ''
2831
+ end
2832
+
2833
+ def options_variable_expression_regexp
2834
+ @delegate_object[:variable_expression_regexp].present? &&
2835
+ Regexp.new(@delegate_object[:variable_expression_regexp])
2836
+ end
2837
+
2808
2838
  def output_color_formatted(data_sym, color_sym)
2809
2839
  formatted_string = string_send_color(@delegate_object[data_sym],
2810
2840
  color_sym)
@@ -2927,9 +2957,24 @@ module MarkdownExec
2927
2957
  # @param opts [Hash] The options hash.
2928
2958
  # @return [Array<Hash>] The updated blocks menu.
2929
2959
  def blocks_as_menu_items(menu_blocks)
2960
+ # prefix first active line, inactive for rest
2961
+ active = @delegate_object[:prompt_margin_left_text]
2962
+ inactive = ' ' * active.length
2963
+
2930
2964
  select_blocks(menu_blocks).map do |fcb|
2931
- fcb.name = fcb.indented_decorated || (fcb.indent + (fcb.s1decorated || fcb.dname))
2932
- fcb.value = fcb.id || fcb.name
2965
+ multiline = fcb.indented_decorated ||
2966
+ (fcb.indent + (fcb.s1decorated || fcb.dname))
2967
+
2968
+ fcb.name = multiline.each_line.with_index.map do |line, index|
2969
+ if fcb.fetch(:disabled, nil).nil?
2970
+ index.zero? ? active : inactive
2971
+ else
2972
+ inactive
2973
+ end + line.chomp
2974
+ end.join("\n")
2975
+
2976
+ fcb.value = fcb.id || fcb.name.split("\n").first
2977
+
2933
2978
  fcb.to_h
2934
2979
  end
2935
2980
  end
@@ -2953,7 +2998,7 @@ module MarkdownExec
2953
2998
  in_block = false
2954
2999
  elsif scan1.present?
2955
3000
  if format1.present?
2956
- caps = NamedCaptureExtractor::extract_named_groups(line, scan1)
3001
+ caps = NamedCaptureExtractor.extract_named_groups(line, scan1)
2957
3002
  if caps
2958
3003
  formatted = format(format1, caps)
2959
3004
  collected_lines << formatted
@@ -3078,6 +3123,18 @@ module MarkdownExec
3078
3123
  sel == MenuOptions::YES
3079
3124
  end
3080
3125
 
3126
+ def prompt_margin_left_text
3127
+ @delegate_object[:prompt_margin_left_text]
3128
+ end
3129
+
3130
+ def prompt_margin_left_width
3131
+ prompt_margin_left_text.length
3132
+ end
3133
+
3134
+ def prompt_margin_right_width
3135
+ 0
3136
+ end
3137
+
3081
3138
  # public
3082
3139
 
3083
3140
  def prompt_select_code_filename(
@@ -3172,8 +3229,11 @@ module MarkdownExec
3172
3229
  history_files(
3173
3230
  @dml_link_state,
3174
3231
  filename:
3175
- asset.present? ? saved_asset_filename(asset,
3176
- @dml_link_state) : filename,
3232
+ if asset.present?
3233
+ saved_asset_filename(asset, @dml_link_state)
3234
+ else
3235
+ filename
3236
+ end,
3177
3237
  path: path
3178
3238
  )&.map do |file|
3179
3239
  unless Regexp.new(regexp) =~ file
@@ -3192,24 +3252,23 @@ module MarkdownExec
3192
3252
  def saved_asset_for_history(
3193
3253
  file:, form:, match_info:
3194
3254
  )
3195
- begin
3196
- OpenStruct.new(
3197
- file: file[(Dir.pwd.length + 1)..-1],
3198
- full: file,
3199
- row: format(
3200
- form,
3201
- # default '*' so unknown parameters are given a wildcard
3202
- match_info.names.each_with_object(Hash.new('*')) do |name, hash|
3203
- hash[name.to_sym] = match_info[name]
3204
- end
3205
- )
3255
+
3256
+ OpenStruct.new(
3257
+ file: file[(Dir.pwd.length + 1)..-1],
3258
+ full: file,
3259
+ row: format(
3260
+ form,
3261
+ # default '*' so unknown parameters are given a wildcard
3262
+ match_info.names.each_with_object(Hash.new('*')) do |name, hash|
3263
+ hash[name.to_sym] = match_info[name]
3264
+ end
3206
3265
  )
3207
- rescue KeyError
3208
- # pp $!, $@
3209
- warn "Cannot format with: #{@delegate_object[:saved_history_format]}"
3210
- error_handler('saved_history_format')
3211
- return :break
3212
- end
3266
+ )
3267
+ rescue KeyError
3268
+ # pp $!, $@
3269
+ warn "Cannot format with: #{@delegate_object[:saved_history_format]}"
3270
+ error_handler('saved_history_format')
3271
+ :break
3213
3272
  end
3214
3273
 
3215
3274
  # Processes YAML data from the selected menu item, updating delegate
@@ -3282,6 +3341,8 @@ module MarkdownExec
3282
3341
  # # opts will be updated with the current console dimensions
3283
3342
  # # and pagination settings.
3284
3343
  def register_console_attributes(opts)
3344
+ return unless IO.console
3345
+
3285
3346
  if (resized = @delegate_object[:menu_resize_terminal])
3286
3347
  resize_terminal
3287
3348
  end
@@ -3305,9 +3366,9 @@ module MarkdownExec
3305
3366
 
3306
3367
  def replace_keys_in_lines(replacement_dictionary, lines)
3307
3368
  # Create a regex pattern that matches any key in the replacement dictionary
3308
- pattern = Regexp.union(replacement_dictionary.keys.map { |key|
3369
+ pattern = Regexp.union(replacement_dictionary.keys.map do |key|
3309
3370
  "%<#{key}>"
3310
- })
3371
+ end)
3311
3372
 
3312
3373
  # Iterate over each line and apply gsub with the replacement hash
3313
3374
  lines.map do |line|
@@ -3429,8 +3490,24 @@ module MarkdownExec
3429
3490
  ).generate_name
3430
3491
  end
3431
3492
 
3493
+ def screen_width
3494
+ if @delegate_object[:screen_width] && @delegate_object[:screen_width].positive?
3495
+ @delegate_object[:screen_width]
3496
+ else
3497
+ @delegate_object[:console_width]
3498
+ end
3499
+ end
3500
+
3501
+ def screen_width_for_table
3502
+ screen_width - prompt_margin_left_width - prompt_margin_right_width - 2 # prompt is symbol + space (width: 2)
3503
+ end
3504
+
3505
+ def screen_width_for_wrapping
3506
+ screen_width_for_table
3507
+ end
3508
+
3432
3509
  def select_document_if_multiple(options, files, prompt:)
3433
- return files if files.class == String ###
3510
+ return files if files.class == String
3434
3511
  return files[0] if (count = files.count) == 1
3435
3512
 
3436
3513
  return unless count >= 2
@@ -3463,9 +3540,20 @@ module MarkdownExec
3463
3540
  props = {
3464
3541
  active_color: active_color_pastel.detach,
3465
3542
  # activate dynamic list searching on letter/number key presses
3543
+ cycle: true,
3466
3544
  filter: true,
3545
+ per_page: @delegate_object[:select_page_height]
3467
3546
  }.freeze
3468
3547
 
3548
+ if menu_items.all? do |item|
3549
+ !item.is_a?(String) && item[:disabled]
3550
+ end
3551
+ menu_items.each do |prompt_item|
3552
+ puts prompt_item[:dname]
3553
+ end
3554
+ return
3555
+ end
3556
+
3469
3557
  # crashes if all menu options are disabled
3470
3558
  # crashes if default is not an existing item
3471
3559
  #
@@ -3484,7 +3572,6 @@ module MarkdownExec
3484
3572
 
3485
3573
  selected = menu_items.find do |item|
3486
3574
  if item.instance_of?(Hash)
3487
- # (item[:id] || item[:name] || item[:dname]) == selection
3488
3575
  [item[:id], item[:name], item[:dname]].include?(selection)
3489
3576
  elsif item.instance_of?(MarkdownExec::FCB)
3490
3577
  item.dname == selection || item.id == selection
@@ -3555,12 +3642,7 @@ module MarkdownExec
3555
3642
  menu_with_back && @link_history.prior_state_exist?
3556
3643
  end
3557
3644
 
3558
- def simple_menu_options(
3559
- per_page: @delegate_object[:select_page_height]
3560
- )
3561
- { cycle: true,
3562
- per_page: per_page }
3563
- end
3645
+ def simple_menu_options; end
3564
3646
 
3565
3647
  # Initializes a new fenced code block (FCB) object based
3566
3648
  # on the provided line and heading information.
@@ -3570,7 +3652,7 @@ module MarkdownExec
3570
3652
  # Regular expression to identify fenced block start.
3571
3653
  # @return [MarkdownExec::FCB] A new FCB instance with the parsed attributes.
3572
3654
  def start_fenced_block(line, headings, fenced_start_extended_regex)
3573
- fcb_title_groups = NamedCaptureExtractor::extract_named_groups(
3655
+ fcb_title_groups = NamedCaptureExtractor.extract_named_groups(
3574
3656
  line, fenced_start_extended_regex
3575
3657
  )
3576
3658
 
@@ -3627,10 +3709,10 @@ module MarkdownExec
3627
3709
  shell: fcb_title_groups.fetch(:shell, ''),
3628
3710
  start_line: line,
3629
3711
  stdin: if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
3630
- NamedCaptureExtractor::extract_named_group2(tn)
3712
+ NamedCaptureExtractor.extract_named_group2(tn)
3631
3713
  end,
3632
3714
  stdout: if (tn = rest.match(/>(?<type>\$)?(?<name>[\w.\-]+)/))
3633
- NamedCaptureExtractor::extract_named_group2(tn)
3715
+ NamedCaptureExtractor.extract_named_group2(tn)
3634
3716
  end,
3635
3717
  title: title,
3636
3718
  type: fcb_title_groups.fetch(:type, ''),
@@ -3736,7 +3818,7 @@ module MarkdownExec
3736
3818
  @dml_block_state = load_cli_or_user_selected_block(
3737
3819
  all_blocks: @dml_blocks_in_file,
3738
3820
  menu_blocks: @dml_menu_blocks,
3739
- default: @dml_menu_default_dname
3821
+ prior_answer: @dml_menu_default_dname
3740
3822
  )
3741
3823
  if !@dml_block_state
3742
3824
  # HashDelegator.error_handler('block_state missing', { abort: true })
@@ -4004,7 +4086,7 @@ module MarkdownExec
4004
4086
  when :parse_document # once for each menu
4005
4087
  vux_parse_document(id: 'vux_parse_document')
4006
4088
  vux_menu_append_history_files(formatted_choice_ostructs,
4007
- id: "vux_menu_append_history_files",)
4089
+ id: 'vux_menu_append_history_files')
4008
4090
  vux_publish_document_file_name_for_external_automation
4009
4091
 
4010
4092
  when :display_menu
@@ -4156,7 +4238,6 @@ module MarkdownExec
4156
4238
 
4157
4239
  # update @delegate_object and @menu_base_options in auto_load
4158
4240
  #
4159
- # @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc, @dml_link_state =
4160
4241
  @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc =
4161
4242
  mdoc_menu_and_blocks_from_nested_files(@dml_link_state, id: id)
4162
4243
  dump_delobj(@dml_blocks_in_file, @dml_menu_blocks, @dml_link_state)
@@ -4220,13 +4301,14 @@ module MarkdownExec
4220
4301
  # user interrupts process
4221
4302
  end
4222
4303
 
4223
- def wait_for_user_selected_block(all_blocks, menu_blocks, default)
4224
- block_state = wait_for_user_selection(all_blocks, menu_blocks, default)
4304
+ def wait_for_user_selected_block(all_blocks, menu_blocks, prior_answer)
4305
+ block_state = wait_for_user_selection(all_blocks, menu_blocks,
4306
+ prior_answer)
4225
4307
  handle_back_or_continue(block_state)
4226
4308
  block_state
4227
4309
  end
4228
4310
 
4229
- def wait_for_user_selection(_all_blocks, menu_blocks, default)
4311
+ def wait_for_user_selection(_all_blocks, menu_blocks, prior_answer)
4230
4312
  if @delegate_object[:clear_screen_for_select_block]
4231
4313
  printf("\e[1;1H\e[2J")
4232
4314
  end
@@ -4242,10 +4324,20 @@ module MarkdownExec
4242
4324
  MenuState::EXIT)
4243
4325
  end
4244
4326
 
4245
- # default value may not match if color is different from
4327
+ selected_answer = case prior_answer
4328
+ when nil
4329
+ nil
4330
+ when String
4331
+ menu_blocks.find { |block|
4332
+ block.dname.include?(prior_answer)
4333
+ }&.name
4334
+ when Struct
4335
+ prior_answer.index || prior_answer.name
4336
+ end
4337
+ # prior_answer value may not match if color is different from
4246
4338
  # originating menu (opts changed while processing)
4247
- selection_opts = if default && menu_blocks.map(&:dname).include?(default)
4248
- @delegate_object.merge(default: default)
4339
+ selection_opts = if selected_answer
4340
+ @delegate_object.merge(default: selected_answer)
4249
4341
  else
4250
4342
  @delegate_object
4251
4343
  end
@@ -4374,6 +4466,10 @@ module MarkdownExec
4374
4466
  def self.next_link_state(*args, **kwargs, &block)
4375
4467
  super
4376
4468
  end
4469
+
4470
+ def self.options_for_tty_menu(options)
4471
+ options.slice(:menu_active_color_pastel_messages, :select_page_height)
4472
+ end
4377
4473
  end
4378
4474
  end
4379
4475
 
@@ -5135,7 +5231,7 @@ module MarkdownExec
5135
5231
 
5136
5232
  def test_iter_blocks_from_nested_files
5137
5233
  @hd.cfile.expect(:readlines, ['line 1', 'line 2'], ['test.md'],
5138
- import_paths: nil)
5234
+ import_paths: '')
5139
5235
  selected_types = ['filtered message']
5140
5236
 
5141
5237
  result = @hd.iter_blocks_from_nested_files { selected_types }