markdown_exec 2.8.3 → 2.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -0
  3. data/Gemfile.lock +1 -1
  4. data/Rakefile +33 -23
  5. data/bats/{block-types.bats → block-type-bash.bats} +0 -25
  6. data/bats/block-type-link.bats +9 -0
  7. data/bats/block-type-port.bats +16 -0
  8. data/bats/block-type-ux-allowed.bats +29 -0
  9. data/bats/block-type-ux-auto.bats +1 -1
  10. data/bats/block-type-ux-chained.bats +9 -0
  11. data/bats/block-type-ux-default.bats +8 -0
  12. data/bats/block-type-ux-echo-hash.bats +14 -0
  13. data/bats/block-type-ux-echo.bats +2 -2
  14. data/bats/block-type-ux-exec.bats +1 -1
  15. data/bats/block-type-ux-hidden.bats +9 -0
  16. data/bats/block-type-ux-invalid.bats +8 -0
  17. data/bats/block-type-ux-transform.bats +1 -1
  18. data/bats/indented-block-type-vars.bats +9 -0
  19. data/bats/line-decor-dynamic.bats +1 -1
  20. data/bats/test_helper.bash +9 -2
  21. data/bats/variable-expansion-multiline.bats +8 -0
  22. data/bats/variable-expansion.bats +1 -1
  23. data/docs/dev/block-type-ux-allowed.md +82 -0
  24. data/docs/dev/block-type-ux-auto.md +2 -1
  25. data/docs/dev/block-type-ux-chained.md +21 -0
  26. data/docs/dev/block-type-ux-default.md +42 -0
  27. data/docs/dev/block-type-ux-echo-hash.md +78 -0
  28. data/docs/dev/block-type-ux-echo.md +3 -1
  29. data/docs/dev/block-type-ux-exec.md +1 -0
  30. data/docs/dev/block-type-ux-hidden.md +21 -0
  31. data/docs/dev/block-type-ux-invalid.md +5 -0
  32. data/docs/dev/block-type-ux-require.md +9 -18
  33. data/docs/dev/indented-block-type-vars.md +6 -0
  34. data/docs/dev/line-decor-dynamic.md +2 -1
  35. data/docs/dev/variable-expansion-multiline.md +31 -0
  36. data/lib/ansi_formatter.rb +0 -1
  37. data/lib/ansi_string.rb +1 -1
  38. data/lib/array.rb +0 -1
  39. data/lib/array_util.rb +0 -1
  40. data/lib/block_label.rb +1 -1
  41. data/lib/cached_nested_file_reader.rb +1 -1
  42. data/lib/constants.rb +18 -0
  43. data/lib/directory_searcher.rb +1 -1
  44. data/lib/exceptions.rb +0 -1
  45. data/lib/fcb.rb +52 -9
  46. data/lib/filter.rb +1 -2
  47. data/lib/format_table.rb +1 -0
  48. data/lib/fout.rb +1 -1
  49. data/lib/hash.rb +0 -1
  50. data/lib/hash_delegator.rb +404 -224
  51. data/lib/link_history.rb +17 -17
  52. data/lib/logged_struct.rb +429 -0
  53. data/lib/markdown_exec/version.rb +1 -1
  54. data/lib/markdown_exec.rb +3 -3
  55. data/lib/mdoc.rb +21 -31
  56. data/lib/menu.src.yml +15 -7
  57. data/lib/menu.yml +11 -6
  58. data/lib/namer.rb +3 -6
  59. data/lib/null_result.rb +131 -0
  60. data/lib/object_present.rb +1 -1
  61. data/lib/option_value.rb +1 -1
  62. data/lib/resize_terminal.rb +1 -2
  63. data/lib/saved_assets.rb +1 -1
  64. data/lib/saved_files_matcher.rb +1 -1
  65. data/lib/shell_session.rb +439 -0
  66. data/lib/streams_out.rb +0 -1
  67. data/lib/string_util.rb +11 -1
  68. data/lib/success_result.rb +112 -0
  69. data/lib/text_analyzer.rb +1 -0
  70. data/lib/ww.rb +9 -7
  71. metadata +25 -3
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env bundle exec ruby
1
+ #!/usr/bin/env -S bundle exec ruby
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # encoding=utf-8
@@ -45,14 +45,6 @@ require_relative 'text_analyzer'
45
45
  $pd = false unless defined?($pd)
46
46
  $table_cell_truncate = true
47
47
 
48
- class String
49
- # Checks if the string is not empty.
50
- # @return [Boolean] Returns true if the string is not empty, false otherwise.
51
- def non_empty?
52
- !empty?
53
- end
54
- end
55
-
56
48
  module HashDelegatorSelf
57
49
  # Applies an ANSI color method to a string using a specified color key.
58
50
  # The method retrieves the color method from the provided hash. If the
@@ -166,7 +158,7 @@ module HashDelegatorSelf
166
158
  # (default is an empty string).
167
159
  # @return [String] A single string with each line indented as specified.
168
160
  def indent_all_lines(body, indent = nil)
169
- return body unless indent&.non_empty?
161
+ return body unless indent&.present?
170
162
 
171
163
  body.lines.map { |line| indent + line.chomp }.join("\n")
172
164
  end
@@ -862,12 +854,15 @@ module MarkdownExec
862
854
 
863
855
  count = 0
864
856
  blocks = []
857
+ results = {}
865
858
  iter_blocks_from_nested_files do |btype, fcb|
866
859
  count += 1
867
860
  case btype
868
861
  when :blocks
862
+ result = SuccessResult.instance
869
863
  if @delegate_object[:bash]
870
- fcb.for_menu!(
864
+ # prepare block for menu, may fail and call HashDelegator.error_handler
865
+ result = fcb.for_menu!(
871
866
  block_calls_scan: @delegate_object[:block_calls_scan],
872
867
  block_name_match: @delegate_object[:block_name_match],
873
868
  block_name_nick_match: @delegate_object[:block_name_nick_match],
@@ -878,10 +873,11 @@ module MarkdownExec
878
873
  ) do |oname, color|
879
874
  apply_block_type_color_option(oname, color)
880
875
  end
876
+ results[fcb.id] = result if result.failure?
881
877
  else
882
878
  expand_references!(fcb, link_state)
883
879
  end
884
- blocks << fcb
880
+ blocks << fcb unless result.failure?
885
881
  when :filter # types accepted
886
882
  %i[blocks line]
887
883
  when :line
@@ -896,9 +892,9 @@ module MarkdownExec
896
892
  end
897
893
  end
898
894
  end
899
- # !!t blocks.count
900
- blocks
895
+ OpenStruct.new(blocks: blocks, results: results)
901
896
  rescue StandardError
897
+ # ww $@, $!,
902
898
  HashDelegator.error_handler('blocks_from_nested_files')
903
899
  end
904
900
 
@@ -914,7 +910,8 @@ module MarkdownExec
914
910
  initial_code_required: false, key_format:
915
911
  )
916
912
  evaluate_shell_expressions(
917
- (link_state&.inherited_lines_block || ''), commands,
913
+ (link_state&.inherited_lines_block || ''),
914
+ commands,
918
915
  initial_code_required: initial_code_required,
919
916
  key_format: key_format
920
917
  )
@@ -990,6 +987,52 @@ module MarkdownExec
990
987
  ]
991
988
  end
992
989
 
990
+ # Executes the allowed exec command and processes the output
991
+ # @param export [Object] The export configuration object
992
+ # @param inherited_code [Array] The inherited code lines
993
+ # @param code_lines [Array] The code lines to append to
994
+ # @param required [Hash] Required code information
995
+ # @return [String, Symbol] The command output or :ux_exec_prohibited if execution failed
996
+ def process_allowed_exec(export, inherited_code, code_lines, required)
997
+ output = export_exec_with_code(
998
+ export, inherited_code, code_lines, required
999
+ )
1000
+ return :ux_exec_prohibited if output == :invalidated
1001
+
1002
+ output.split("\n")
1003
+ end
1004
+
1005
+ def code_from_automatic_ux_blocks(
1006
+ all_blocks,
1007
+ mdoc
1008
+ )
1009
+ unless @ux_most_recent_filename != @delegate_object[:filename]
1010
+ return
1011
+ end
1012
+
1013
+ blocks = select_automatic_ux_blocks(all_blocks)
1014
+ if blocks.empty?
1015
+ blocks = select_automatic_ux_blocks(all_blocks)
1016
+ end
1017
+ return if blocks.empty?
1018
+
1019
+ @ux_most_recent_filename = @delegate_object[:filename]
1020
+
1021
+ (blocks.each.with_object([]) do |block, merged_options|
1022
+ code = code_from_ux_block_to_set_environment_variables(
1023
+ block,
1024
+ mdoc,
1025
+ force: @delegate_object[:ux_auto_load_force_default],
1026
+ only_default: true
1027
+ )
1028
+ if code == :ux_exec_prohibited
1029
+ merged_options
1030
+ else
1031
+ merged_options.push(code)
1032
+ end
1033
+ end).to_a
1034
+ end
1035
+
993
1036
  # parse YAML body defining the UX for a single variable
994
1037
  # set ENV value for the variable and return code lines for the same
995
1038
  def code_from_ux_block_to_set_environment_variables(
@@ -1023,105 +1066,30 @@ module MarkdownExec
1023
1066
  code_lines.push "[[ -z $#{precondition} ]] && exit 1"
1024
1067
  end
1025
1068
 
1026
- exportable = true
1027
1069
  if only_default
1028
- value = case export.default
1029
- # echo > default
1030
- when :echo
1031
- raise unless export.echo.present?
1032
-
1033
- output = export_echo_with_code(
1034
- export, inherited_code, code_lines, required
1035
- )
1036
- if output == :invalidated
1037
- return :ux_exec_prohibited
1038
- end
1039
-
1040
- transform_export_value(output, export)
1041
-
1042
- # exec > default
1043
- when :exec
1044
- raise unless export.exec.present?
1045
-
1046
- output = export_exec_with_code(
1047
- export, inherited_code, code_lines, required
1048
- )
1049
- if output == :invalidated
1050
- return :ux_exec_prohibited
1051
- end
1052
-
1053
- transform_export_value(output, export)
1054
-
1055
- # default
1056
- else
1057
- export.default.to_s
1058
- end
1059
- else
1060
- value = nil
1061
-
1062
- # echo > exec
1063
- if export.echo
1064
- value = export_echo_with_code(
1070
+ value, exportable, transformable =
1071
+ ux_block_export_automatic(
1065
1072
  export, inherited_code, code_lines, required
1066
1073
  )
1067
- if value == :invalidated
1068
- return :ux_exec_prohibited
1069
- end
1070
-
1071
- # exec > allowed
1072
- elsif export.exec
1073
- value = export_exec_with_code(
1074
- export, inherited_code, code_lines, required
1074
+ else
1075
+ value, exportable, transformable =
1076
+ ux_block_export_activated(
1077
+ export, inherited_code, code_lines, required, exit_prompt
1075
1078
  )
1076
- if value == :invalidated
1077
- return :ux_exec_prohibited
1078
- end
1079
-
1080
- # allowed > prompt
1081
- elsif export.allowed && export.allowed.count.positive?
1082
- case (choice = prompt_select_code_filename(
1083
- [exit_prompt] + export.allowed,
1084
- string: export.prompt,
1085
- color_sym: :prompt_color_after_script_execution
1086
- ))
1087
- when exit_prompt
1088
- exportable = false
1089
- else
1090
- value = choice
1091
- end
1092
-
1093
- # prompt > default
1094
- elsif export.prompt.present?
1095
- begin
1096
- loop do
1097
- print "#{export.prompt} [#{export.default}]: "
1098
- value = gets.chomp
1099
- value = export.default.to_s if value.empty?
1100
- caps = NamedCaptureExtractor.extract_named_groups(value,
1101
- export.validate)
1102
- break if caps
1103
-
1104
- # invalid input, retry
1105
- end
1106
- rescue Interrupt
1107
- exportable = false
1108
- end
1079
+ end
1080
+ return :ux_exec_prohibited if value == :ux_exec_prohibited
1109
1081
 
1110
- # default
1111
- else
1112
- value = export.default
1082
+ if SelectResponse.continue?(value)
1083
+ if transformable
1084
+ value = transform_export_value(value, export)
1113
1085
  end
1114
1086
 
1115
1087
  if exportable
1116
- value = transform_export_value(value, export)
1088
+ ENV[export.name] = value.to_s
1089
+ code_lines.push code_line_safe_assign(export.name, value,
1090
+ force: force)
1117
1091
  end
1118
1092
  end
1119
-
1120
- if exportable
1121
- ENV[export.name] = value.to_s
1122
- code_lines.push code_line_safe_assign(export.name, value,
1123
- force: force)
1124
- end
1125
1093
  else
1126
1094
  raise "Invalid data type: #{data.inspect}"
1127
1095
  end
@@ -1363,7 +1331,7 @@ module MarkdownExec
1363
1331
  )
1364
1332
  # Initialize a counter for named group occurrences
1365
1333
  occurrence_count = Hash.new(0)
1366
- return occurrence_count if pattern == //
1334
+ return occurrence_count if pattern.nil? || pattern == //
1367
1335
 
1368
1336
  blocks.each do |block|
1369
1337
  # Skip processing for shell-type blocks
@@ -1381,7 +1349,7 @@ module MarkdownExec
1381
1349
 
1382
1350
  def count_named_group_occurrences_block_body_fix_indent(block)
1383
1351
  ### actually double the entries, but not a problem since it's used as a boolean
1384
- ([block.oname || ''] + block.body).join("\n")
1352
+ ([block.oname || ''] + (block.body || [''])).join("\n")
1385
1353
  end
1386
1354
 
1387
1355
  ##
@@ -1425,10 +1393,30 @@ module MarkdownExec
1425
1393
  line_cap[:line] ||= ''
1426
1394
 
1427
1395
  line_caps = [line_cap]
1428
- if wrap && line_cap[:text].length > screen_width_for_wrapping
1429
- wrapper = StringWrapper.new(width: screen_width_for_wrapping - line_cap[:indent].length)
1430
- line_caps = wrapper.wrap(line_cap[:text]).map do |wrapped_line|
1431
- line_cap.dup.merge(text: wrapped_line)
1396
+
1397
+ # split text with newlines, from variable expansion
1398
+ if line_cap[:text].include?("\n")
1399
+ lines = line_cap[:text].split("\n")
1400
+ line_caps = lines.map do |line|
1401
+ line_cap.dup.merge(text: line)
1402
+ end.to_a
1403
+ end
1404
+
1405
+ # wrap text on multiple lines to screen width, replacing line_caps
1406
+ if wrap
1407
+ line_caps = line_caps.flat_map do |line_cap|
1408
+ text = line_cap[:text]
1409
+ wrapper = StringWrapper.new(width: screen_width_for_wrapping - line_cap[:indent].length)
1410
+
1411
+ if text.length > screen_width_for_wrapping
1412
+ # Wrap this text and create line_cap objects for each part
1413
+ wrapper.wrap(text).map do |wrapped_text|
1414
+ line_cap.dup.merge(text: wrapped_text)
1415
+ end
1416
+ else
1417
+ # No change needed for this line
1418
+ line_cap
1419
+ end
1432
1420
  end
1433
1421
  end
1434
1422
 
@@ -1527,7 +1515,11 @@ module MarkdownExec
1527
1515
  if block_given?
1528
1516
  # expand references only if block is recognized (not a comment)
1529
1517
  yield if block_given?
1530
- mbody = fcb.body[0].match @delegate_object[criteria[:match]]
1518
+
1519
+ # parse multiline to capture output of variable expansion
1520
+ mbody = fcb.body[0].match Regexp.new(
1521
+ @delegate_object[criteria[:match]], Regexp::MULTILINE
1522
+ )
1531
1523
  end
1532
1524
 
1533
1525
  create_and_add_chrome_block(
@@ -2432,7 +2424,8 @@ module MarkdownExec
2432
2424
  # update blocks
2433
2425
  #
2434
2426
  Regexp.union(replacements.keys.map do |word|
2435
- Regexp.new(Regexp.escape(word))
2427
+ # match multiline text from variable expansion
2428
+ Regexp.new(Regexp.escape(word), Regexp::MULTILINE)
2436
2429
  end).tap do |pattern|
2437
2430
  menu_blocks.each do |block|
2438
2431
  next if exclude_types.include?(block.type)
@@ -2446,14 +2439,16 @@ module MarkdownExec
2446
2439
  expand_variable_references!(
2447
2440
  blocks: [fcb],
2448
2441
  initial_code_required: false,
2449
- link_state: link_state
2442
+ key_format: @delegate_object[:variable_expression_format],
2443
+ link_state: link_state,
2444
+ pattern: options_variable_expression_regexp
2450
2445
  )
2451
2446
  expand_variable_references!(
2452
2447
  blocks: [fcb],
2453
2448
  echo_format: '%s',
2454
2449
  group_name: :command,
2455
2450
  initial_code_required: false,
2456
- key_format: '$(%s)',
2451
+ key_format: @delegate_object[:command_substitution_format],
2457
2452
  link_state: link_state,
2458
2453
  pattern: options_command_substitution_regexp
2459
2454
  )
@@ -2461,16 +2456,13 @@ module MarkdownExec
2461
2456
 
2462
2457
  def expand_variable_references!(
2463
2458
  blocks:,
2464
- echo_format: 'echo $%s',
2459
+ echo_format: 'echo "$%s"',
2465
2460
  group_name: :variable,
2466
2461
  initial_code_required: false,
2467
- key_format: '${%s}',
2462
+ key_format:,
2468
2463
  link_state:,
2469
- pattern: nil
2464
+ pattern:
2470
2465
  )
2471
- pattern ||= options_variable_expression_regexp
2472
- return if pattern.nil?
2473
-
2474
2466
  variable_counts = count_named_group_occurrences(blocks, pattern,
2475
2467
  group_name: group_name)
2476
2468
  return if variable_counts.nil? || variable_counts == {}
@@ -2489,9 +2481,11 @@ module MarkdownExec
2489
2481
  expand_blocks_with_replacements(blocks, replacements)
2490
2482
  end
2491
2483
 
2492
- def export_echo_with_code(export, inherited_code, code_lines, required)
2484
+ def export_echo_with_code_single(export_echo, inherited_code, code_lines,
2485
+ required)
2486
+ code = %(printf '%s' "#{export_echo}")
2493
2487
  value = execute_temporary_script(
2494
- %Q{eval printf '%s' "#{export.echo}"},
2488
+ code,
2495
2489
  (inherited_code || []) +
2496
2490
  code_lines + required[:code]
2497
2491
  )
@@ -2501,6 +2495,26 @@ module MarkdownExec
2501
2495
  value
2502
2496
  end
2503
2497
 
2498
+ def export_echo_with_code(export, inherited_code, code_lines, required,
2499
+ force:)
2500
+ exportable = true
2501
+ case export.echo
2502
+ when String, Integer, Float, TrueClass, FalseClass
2503
+ value = export_echo_with_code_single(export.echo.to_s, inherited_code,
2504
+ code_lines, required)
2505
+ when Hash
2506
+ # each item in the hash is a variable name and value
2507
+ export.echo.each do |name, expression|
2508
+ value = export_echo_with_code_single(expression, inherited_code,
2509
+ code_lines, required)
2510
+ ENV[name] = value.to_s
2511
+ code_lines.push code_line_safe_assign(name, value, force: force)
2512
+ end
2513
+ exportable = false
2514
+ end
2515
+ [value, exportable]
2516
+ end
2517
+
2504
2518
  def export_exec_with_code(export, inherited_code, code_lines, required)
2505
2519
  value = execute_temporary_script(
2506
2520
  export.exec,
@@ -2555,7 +2569,7 @@ module MarkdownExec
2555
2569
  end
2556
2570
 
2557
2571
  @dml_blocks_in_file.find(&match_block) ||
2558
- @dml_menu_blocks.find(&match_block)
2572
+ @dml_menu_blocks.find(&match_block)
2559
2573
  end
2560
2574
 
2561
2575
  # find a block by its original (undecorated) name or nickname (not visible in menu)
@@ -2763,7 +2777,7 @@ module MarkdownExec
2763
2777
  menu_entries, current_display_format
2764
2778
  )
2765
2779
 
2766
- selection = prompt_select_code_filename(
2780
+ selection = prompt_select_from_list(
2767
2781
  menu_options,
2768
2782
  string: menu_title,
2769
2783
  color_sym: :prompt_color_after_script_execution
@@ -2819,7 +2833,7 @@ module MarkdownExec
2819
2833
  def iter_source_blocks(source, source_id: nil, &block)
2820
2834
  case source
2821
2835
  when 1
2822
- blocks_from_nested_files(source_id: source_id).each(&block)
2836
+ blocks_from_nested_files(source_id: source_id).blocks.each(&block)
2823
2837
  when 2
2824
2838
  @dml_blocks_in_file.each(&block)
2825
2839
  when 3
@@ -2862,7 +2876,8 @@ module MarkdownExec
2862
2876
  nil),
2863
2877
  end_pattern: @delegate_object.fetch(:output_assignment_end, nil),
2864
2878
  scan1: @delegate_object.fetch(:output_assignment_match, nil),
2865
- format1: @delegate_object.fetch(:output_assignment_format, nil)
2879
+ format1: @delegate_object.fetch(:output_assignment_format, nil),
2880
+ name: ''
2866
2881
  )
2867
2882
 
2868
2883
  else
@@ -2969,69 +2984,45 @@ module MarkdownExec
2969
2984
  @fout.fout_list(list)
2970
2985
  end
2971
2986
 
2972
- # Loads auto blocks based on delegate object settings and updates
2973
- # if new filename is detected.
2974
- # Executes a specified block once per filename.
2975
- # @param all_blocks [Array] Array of all block elements.
2976
- # @return [Boolean, nil] True if values were modified, nil otherwise.
2987
+ # Loads and updates auto options for document blocks if the current filename has changed.
2988
+ #
2989
+ # This method checks if the delegate object specifies a document load options block name and if the filename
2990
+ # has been updated. It then selects the appropriate blocks, collects their dependencies, processes their
2991
+ # options, and updates the menu base with the merged options.
2992
+ #
2993
+ # @param all_blocks [Array] An array of all block elements.
2994
+ # @param mdoc [Object] The document object managing dependencies and options.
2995
+ # @return [Boolean, nil] Returns true if options were updated; nil otherwise.
2977
2996
  def load_auto_opts_block(all_blocks, mdoc:)
2978
- block_name = @delegate_object[:document_load_opts_block_name]
2979
- unless block_name.present? &&
2980
- @opts_most_recent_filename != @delegate_object[:filename]
2981
- return
2982
- end
2997
+ opts_block_name = @delegate_object[:document_load_opts_block_name]
2998
+ current_filename = @delegate_object[:filename]
2983
2999
 
2984
- blocks = HashDelegator.block_select(all_blocks, :oname, block_name)
2985
- return if blocks.empty?
3000
+ return unless opts_block_name.present? &&
3001
+ @opts_most_recent_filename != current_filename
3002
+
3003
+ selected_blocks = HashDelegator.block_select(all_blocks, :oname,
3004
+ opts_block_name)
3005
+ return if selected_blocks.empty?
3006
+
3007
+ dependency_map = {}
3008
+ selected_blocks.each do |block|
3009
+ mdoc.collect_dependencies(memo: dependency_map, block: block)
3010
+ end
2986
3011
 
2987
- update_menu_base(
2988
- blocks.each.with_object({}) do |block, merged_options|
3012
+ collected_options =
3013
+ dependency_map.each.with_object({}) do |(block_id, _), merged_options|
3014
+ matching_block = HashDelegator.block_find(all_blocks, :id, block_id)
2989
3015
  options_state = read_show_options_and_trigger_reuse(
2990
- mdoc: mdoc,
2991
- selected: block
3016
+ mdoc: mdoc, selected: matching_block
2992
3017
  )
2993
3018
  merged_options.merge!(options_state.options)
2994
3019
  end
2995
- )
2996
3020
 
2997
- @opts_most_recent_filename = @delegate_object[:filename]
3021
+ update_menu_base(collected_options)
3022
+ @opts_most_recent_filename = current_filename
2998
3023
  true
2999
3024
  end
3000
3025
 
3001
- def load_auto_ux_block(
3002
- all_blocks,
3003
- mdoc,
3004
- block_name: @delegate_object[:document_load_ux_block_name]
3005
- )
3006
- unless block_name.present? &&
3007
- @ux_most_recent_filename != @delegate_object[:filename]
3008
- return
3009
- end
3010
-
3011
- blocks = HashDelegator.block_select(all_blocks, :oname, block_name)
3012
- if blocks.empty?
3013
- blocks = HashDelegator.block_match(all_blocks, :nickname,
3014
- Regexp.new(block_name))
3015
- end
3016
- return if blocks.empty?
3017
-
3018
- @ux_most_recent_filename = @delegate_object[:filename]
3019
-
3020
- (blocks.each.with_object([]) do |block, merged_options|
3021
- code = code_from_ux_block_to_set_environment_variables(
3022
- block,
3023
- mdoc,
3024
- force: @delegate_object[:ux_auto_load_force_default],
3025
- only_default: true
3026
- )
3027
- if code == :ux_exec_prohibited
3028
- merged_options
3029
- else
3030
- merged_options.push(code)
3031
- end
3032
- end).to_a
3033
- end
3034
-
3035
3026
  def load_auto_vars_block(
3036
3027
  all_blocks,
3037
3028
  block_name: @delegate_object[:document_load_vars_block_name]
@@ -3123,7 +3114,7 @@ module MarkdownExec
3123
3114
  else
3124
3115
  ## user selects from existing files or other
3125
3116
  #
3126
- case (name = prompt_select_code_filename(
3117
+ case (name = prompt_select_from_list(
3127
3118
  [@delegate_object[:prompt_filespec_back]] + files,
3128
3119
  string: @delegate_object[:prompt_select_code_file],
3129
3120
  color_sym: :prompt_color_after_script_execution
@@ -3154,11 +3145,19 @@ module MarkdownExec
3154
3145
  end
3155
3146
 
3156
3147
  def mdoc_and_blocks_from_nested_files(source_id: nil)
3157
- menu_blocks = blocks_from_nested_files(source_id: source_id)
3158
- mdoc = MDoc.new(menu_blocks) do |nopts|
3148
+ blocks_results = blocks_from_nested_files(source_id: source_id)
3149
+
3150
+ blocks_results.results.select do |_id, result|
3151
+ result.failure?
3152
+ end.each do |id, result|
3153
+ HashDelegator.error_handler("#{id} - #{result.to_yaml}")
3154
+ end
3155
+
3156
+ mdoc = MDoc.new(blocks_results.blocks) do |nopts|
3159
3157
  @delegate_object.merge!(nopts)
3160
3158
  end
3161
- [menu_blocks, mdoc]
3159
+
3160
+ [blocks_results.blocks, mdoc]
3162
3161
  end
3163
3162
 
3164
3163
  ## Handles the file loading and returns the blocks
@@ -3184,7 +3183,7 @@ module MarkdownExec
3184
3183
 
3185
3184
  # load document ux block
3186
3185
  #
3187
- if (code_lines = load_auto_ux_block(all_blocks, mdoc))
3186
+ if (code_lines = code_from_automatic_ux_blocks(all_blocks, mdoc))
3188
3187
  new_code = HashDelegator.code_merge(link_state.inherited_lines,
3189
3188
  code_lines)
3190
3189
  next_state_set_code(nil, link_state, new_code)
@@ -3305,6 +3304,19 @@ module MarkdownExec
3305
3304
  end
3306
3305
  end
3307
3306
 
3307
+ def menu_from_list_with_back(list)
3308
+ case (name = prompt_select_from_list(
3309
+ [@delegate_object[:prompt_filespec_back]] + list,
3310
+ string: @delegate_object[:prompt_select_code_file],
3311
+ color_sym: :prompt_color_after_script_execution
3312
+ ))
3313
+ when @delegate_object[:prompt_filespec_back]
3314
+ SelectResponse::BACK
3315
+ else
3316
+ name
3317
+ end
3318
+ end
3319
+
3308
3320
  def menu_toggle_collapsible_block(selected)
3309
3321
  # return true if @compress_ids.key?(fcb.id) && !!@compress_ids[fcb.id]
3310
3322
  # return false if @expand_ids.key?(fcb.id) && !!@expand_ids[fcb.id]
@@ -3487,6 +3499,11 @@ module MarkdownExec
3487
3499
  fout_execution_report if @delegate_object[:output_execution_report]
3488
3500
  end
3489
3501
 
3502
+ # all UX blocks are automatic for the document
3503
+ def select_automatic_ux_blocks(blocks)
3504
+ blocks.select { |item| item.type == 'ux' }
3505
+ end
3506
+
3490
3507
  # Filter blocks per block_name_include_match, block_name_wrapper_match.
3491
3508
  #
3492
3509
  # @param all_blocks [Array<Hash>] The list of blocks from the file.
@@ -3538,7 +3555,7 @@ module MarkdownExec
3538
3555
  # private
3539
3556
 
3540
3557
  def process_string_array(arr, begin_pattern: nil, end_pattern: nil, scan1: nil,
3541
- format1: nil)
3558
+ format1: nil, name: '')
3542
3559
  in_block = !begin_pattern.present?
3543
3560
  collected_lines = []
3544
3561
 
@@ -3560,6 +3577,9 @@ module MarkdownExec
3560
3577
  collected_lines << formatted
3561
3578
  end
3562
3579
  end
3580
+ elsif format1.present?
3581
+ formatted = format(format1, { value: line })
3582
+ collected_lines << formatted
3563
3583
  else
3564
3584
  collected_lines << line
3565
3585
  end
@@ -3685,9 +3705,22 @@ module MarkdownExec
3685
3705
  0
3686
3706
  end
3687
3707
 
3708
+ def prompt_select_continue(filter: true, quiet: true)
3709
+ sel = @prompt.select(
3710
+ string_send_color(@delegate_object[:prompt_after_script_execution],
3711
+ :prompt_color_after_script_execution),
3712
+ filter: filter,
3713
+ quiet: quiet
3714
+ ) do |menu|
3715
+ menu.choice @delegate_object[:prompt_yes]
3716
+ menu.choice @delegate_object[:prompt_exit]
3717
+ end
3718
+ sel == @delegate_object[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
3719
+ end
3720
+
3688
3721
  # public
3689
3722
 
3690
- def prompt_select_code_filename(
3723
+ def prompt_select_from_list(
3691
3724
  filenames,
3692
3725
  color_sym: :prompt_color_after_script_execution,
3693
3726
  cycle: true,
@@ -3713,19 +3746,6 @@ module MarkdownExec
3713
3746
  end
3714
3747
  end
3715
3748
 
3716
- def prompt_select_continue(filter: true, quiet: true)
3717
- sel = @prompt.select(
3718
- string_send_color(@delegate_object[:prompt_after_script_execution],
3719
- :prompt_color_after_script_execution),
3720
- filter: filter,
3721
- quiet: quiet
3722
- ) do |menu|
3723
- menu.choice @delegate_object[:prompt_yes]
3724
- menu.choice @delegate_object[:prompt_exit]
3725
- end
3726
- sel == @delegate_object[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
3727
- end
3728
-
3729
3749
  # user prompt to exit if the menu will be displayed again
3730
3750
  #
3731
3751
  def prompt_user_exit(block_name_from_cli:, selected:)
@@ -3799,27 +3819,6 @@ module MarkdownExec
3799
3819
  end&.compact
3800
3820
  end
3801
3821
 
3802
- def saved_asset_for_history(
3803
- file:, form:, match_info:
3804
- )
3805
- OpenStruct.new(
3806
- file: file[(Dir.pwd.length + 1)..-1],
3807
- full: file,
3808
- row: format(
3809
- form,
3810
- # default '*' so unknown parameters are given a wildcard
3811
- match_info.names.each_with_object(Hash.new('*')) do |name, hash|
3812
- hash[name.to_sym] = match_info[name]
3813
- end
3814
- )
3815
- )
3816
- rescue KeyError
3817
- # pp $!, $@
3818
- warn "Cannot format with: #{@delegate_object[:saved_history_format]}"
3819
- error_handler('saved_history_format')
3820
- :break
3821
- end
3822
-
3823
3822
  # Processes YAML data from the selected menu item, updating delegate
3824
3823
  # objects and optionally printing formatted output.
3825
3824
  # @param selected [Hash] Selected item from the menu containing a YAML body.
@@ -3874,8 +3873,8 @@ module MarkdownExec
3874
3873
  # console [height, width]. If not provided or if the terminal
3875
3874
  # is resized, it will be set to the current console dimensions.
3876
3875
  # - :select_page_height [Integer, nil] The height of the page for
3877
- # selection. If not provided or if not positive, it will be set
3878
- # to the maximum of (console height - 3) or 4.
3876
+ # selection. If not provided or if not positive, it
3877
+ # will be set to the maximum of (console height - 3) or 4.
3879
3878
  # - :per_page [Integer, nil] The number of items per page. If
3880
3879
  # :select_page_height is not provided or if not positive, it
3881
3880
  # will be set to the maximum of (console height - 3) or 4.
@@ -4002,7 +4001,7 @@ module MarkdownExec
4002
4001
  ## user selects from existing files or other
4003
4002
  # input into path with wildcard for easy entry
4004
4003
  #
4005
- case (name = prompt_select_code_filename(
4004
+ case (name = prompt_select_from_list(
4006
4005
  [@delegate_object[:prompt_filespec_back],
4007
4006
  @delegate_object[:prompt_filespec_other]] + files,
4008
4007
  string: @delegate_object[:prompt_select_code_file],
@@ -4039,6 +4038,27 @@ module MarkdownExec
4039
4038
  ).generate_name
4040
4039
  end
4041
4040
 
4041
+ def saved_asset_for_history(
4042
+ file:, form:, match_info:
4043
+ )
4044
+ OpenStruct.new(
4045
+ file: file[(Dir.pwd.length + 1)..-1],
4046
+ full: file,
4047
+ row: format(
4048
+ form,
4049
+ # default '*' so unknown parameters are given a wildcard
4050
+ match_info.names.each_with_object(Hash.new('*')) do |name, hash|
4051
+ hash[name.to_sym] = match_info[name]
4052
+ end
4053
+ )
4054
+ )
4055
+ rescue KeyError
4056
+ # pp $!, $@
4057
+ warn "Cannot format with: #{@delegate_object[:saved_history_format]}"
4058
+ error_handler('saved_history_format')
4059
+ :break
4060
+ end
4061
+
4042
4062
  def screen_width
4043
4063
  width = @delegate_object[:screen_width]
4044
4064
  if width&.positive?
@@ -4387,6 +4407,166 @@ module MarkdownExec
4387
4407
  @delegate_object.merge!(options)
4388
4408
  end
4389
4409
 
4410
+ def ux_block_export_activated(export, inherited_code, code_lines, required,
4411
+ exit_prompt)
4412
+ exportable = true
4413
+ transformable = true
4414
+ [if export.allowed.present?
4415
+ if export.allowed == :echo
4416
+ output, exportable = export_echo_with_code(
4417
+ export, inherited_code, code_lines, required, force: true
4418
+ )
4419
+ return :ux_exec_prohibited if output == :invalidated
4420
+
4421
+ menu_from_list_with_back(output.split("\n"))
4422
+
4423
+ elsif export.allowed == :exec
4424
+ output = process_allowed_exec(export, inherited_code,
4425
+ code_lines, required)
4426
+ return :ux_exec_prohibited if output == :ux_exec_prohibited
4427
+
4428
+ menu_from_list_with_back(output)
4429
+
4430
+ else
4431
+ menu_from_list_with_back(export.allowed)
4432
+ end
4433
+
4434
+ # echo > exec
4435
+ elsif export.echo
4436
+ output, exportable = export_echo_with_code(
4437
+ export, inherited_code, code_lines, required, force: true
4438
+ )
4439
+ return :ux_exec_prohibited if output == :invalidated
4440
+
4441
+ output
4442
+
4443
+ # exec > allowed
4444
+ elsif export.exec
4445
+ output = export_exec_with_code(
4446
+ export, inherited_code, code_lines, required
4447
+ )
4448
+ return :ux_exec_prohibited if output == :invalidated
4449
+
4450
+ output
4451
+
4452
+ # allowed > prompt
4453
+ elsif export.allowed && export.allowed.count.positive?
4454
+ case (choice = prompt_select_from_list(
4455
+ [exit_prompt] + export.allowed,
4456
+ string: export.prompt,
4457
+ color_sym: :prompt_color_after_script_execution
4458
+ ))
4459
+ when exit_prompt
4460
+ exportable = false
4461
+ transformable = false
4462
+ nil
4463
+ else
4464
+ choice
4465
+ end
4466
+
4467
+ # prompt > default
4468
+ elsif export.prompt.present?
4469
+ begin
4470
+ loop do
4471
+ print "#{export.prompt} [#{export.default}]: "
4472
+ output = gets.chomp
4473
+ output = export.default.to_s if output.empty?
4474
+ caps = NamedCaptureExtractor.extract_named_groups(output,
4475
+ export.validate)
4476
+ break if caps
4477
+
4478
+ # invalid input, retry
4479
+ end
4480
+ rescue Interrupt
4481
+ exportable = false
4482
+ transformable = false
4483
+ end
4484
+
4485
+ output
4486
+
4487
+ # default
4488
+ else
4489
+ transformable = false
4490
+ export.default
4491
+ end, exportable, transformable]
4492
+ end
4493
+
4494
+ def ux_block_export_automatic(export, inherited_code, code_lines, required)
4495
+ transformable = true
4496
+ exportable = true
4497
+
4498
+ if export.default == false
4499
+ exportable = false
4500
+ transformable = false
4501
+ value = nil
4502
+ else
4503
+ if export.default.nil?
4504
+ if export.echo.present?
4505
+ export.default = :echo
4506
+ elsif export.exec.present?
4507
+ export.default = :exec
4508
+ elsif export.allowed.present?
4509
+ export.default = export.allowed.first
4510
+ end
4511
+ end
4512
+
4513
+ value = case export.default
4514
+ when :allowed
4515
+ raise unless export.allowed.present?
4516
+
4517
+ if export.allowed == :echo
4518
+ output, exportable = export_echo_with_code(
4519
+ export, inherited_code, code_lines, required, force: false
4520
+ )
4521
+ return :ux_exec_prohibited if output == :invalidated
4522
+
4523
+ exportable && output.split("\n").first
4524
+
4525
+ elsif export.allowed == :exec
4526
+ output = process_allowed_exec(export, inherited_code,
4527
+ code_lines, required)
4528
+ return :ux_exec_prohibited if output == :ux_exec_prohibited
4529
+
4530
+ output.first
4531
+
4532
+ else
4533
+ export.allowed.first
4534
+ end
4535
+
4536
+ # echo > default
4537
+ when :echo
4538
+ raise unless export.echo.present?
4539
+
4540
+ output, exportable = export_echo_with_code(
4541
+ export, inherited_code, code_lines, required, force: false
4542
+ )
4543
+ return :ux_exec_prohibited if output == :invalidated
4544
+
4545
+ output
4546
+
4547
+ # exec > default
4548
+ when :exec
4549
+ raise unless export.exec.present?
4550
+
4551
+ output = export_exec_with_code(
4552
+ export, inherited_code, code_lines, required
4553
+ )
4554
+ return :ux_exec_prohibited if output == :invalidated
4555
+
4556
+ output
4557
+
4558
+ # default
4559
+ else
4560
+ transformable = false
4561
+ export.default.to_s
4562
+ end
4563
+ end
4564
+
4565
+ [value,
4566
+ exportable,
4567
+ transformable]
4568
+ end
4569
+
4390
4570
  def vux_await_user_selection(prior_answer: @dml_block_selection)
4391
4571
  @dml_block_state = load_cli_or_user_selected_block(
4392
4572
  all_blocks: @dml_blocks_in_file,
@@ -5324,7 +5504,7 @@ module MarkdownExec
5324
5504
  end
5325
5505
 
5326
5506
  def test_blocks_from_nested_files
5327
- result = @hd.blocks_from_nested_files
5507
+ result = @hd.blocks_from_nested_files.blocks
5328
5508
  assert_kind_of Array, result
5329
5509
  assert_kind_of FCB, result.first
5330
5510
  end
@@ -5333,7 +5513,7 @@ module MarkdownExec
5333
5513
  @hd = HashDelegator.new(no_chrome: true)
5334
5514
  @hd.expects(:create_and_add_chrome_blocks).never
5335
5515
 
5336
- result = @hd.blocks_from_nested_files
5516
+ result = @hd.blocks_from_nested_files.blocks
5337
5517
 
5338
5518
  assert_kind_of Array, result
5339
5519
  end