markdown_exec 2.8.2 → 2.8.4

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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -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-echo-hash.bats +14 -0
  12. data/bats/block-type-ux-echo.bats +20 -0
  13. data/bats/block-type-ux-exec.bats +1 -1
  14. data/bats/block-type-ux-hidden.bats +9 -0
  15. data/bats/block-type-ux-invalid.bats +8 -0
  16. data/bats/block-type-ux-preconditions.bats +8 -0
  17. data/bats/block-type-ux-readonly.bats +10 -0
  18. data/bats/block-type-ux-transform.bats +1 -1
  19. data/bats/block-type-vars.bats +3 -3
  20. data/bats/indented-block-type-vars.bats +9 -0
  21. data/bats/indented-multi-line-output.bats +9 -0
  22. data/bats/line-decor-dynamic.bats +8 -0
  23. data/bats/test_helper.bash +19 -2
  24. data/bats/variable-expansion-multiline.bats +8 -0
  25. data/bats/variable-expansion.bats +1 -1
  26. data/bin/tab_completion.sh +0 -5
  27. data/bin/tab_completion.sh.erb +0 -5
  28. data/docs/dev/block-type-ux-allowed.md +80 -0
  29. data/docs/dev/block-type-ux-chained.md +21 -0
  30. data/docs/dev/block-type-ux-echo-hash.md +72 -0
  31. data/docs/dev/block-type-ux-echo.md +21 -0
  32. data/docs/dev/block-type-ux-hidden.md +21 -0
  33. data/docs/dev/block-type-ux-invalid.md +5 -0
  34. data/docs/dev/block-type-ux-preconditions.md +9 -0
  35. data/docs/dev/block-type-ux-readonly.md +7 -0
  36. data/docs/dev/block-type-ux-require.md +8 -4
  37. data/docs/dev/indented-block-type-vars.md +6 -0
  38. data/docs/dev/indented-multi-line-output.md +11 -0
  39. data/docs/dev/line-decor-dynamic.md +10 -0
  40. data/docs/dev/variable-expansion-multiline.md +31 -0
  41. data/lib/ansi_formatter.rb +0 -1
  42. data/lib/ansi_string.rb +10 -1
  43. data/lib/array.rb +0 -1
  44. data/lib/array_util.rb +0 -1
  45. data/lib/block_label.rb +1 -1
  46. data/lib/cached_nested_file_reader.rb +1 -1
  47. data/lib/constants.rb +18 -0
  48. data/lib/directory_searcher.rb +1 -1
  49. data/lib/exceptions.rb +0 -1
  50. data/lib/fcb.rb +51 -8
  51. data/lib/filter.rb +4 -4
  52. data/lib/format_table.rb +1 -0
  53. data/lib/fout.rb +1 -1
  54. data/lib/hash.rb +0 -1
  55. data/lib/hash_delegator.rb +403 -200
  56. data/lib/link_history.rb +17 -17
  57. data/lib/logged_struct.rb +429 -0
  58. data/lib/markdown_exec/version.rb +1 -1
  59. data/lib/markdown_exec.rb +3 -3
  60. data/lib/mdoc.rb +5 -17
  61. data/lib/menu.src.yml +3 -1
  62. data/lib/menu.yml +1 -1
  63. data/lib/namer.rb +4 -5
  64. data/lib/null_result.rb +131 -0
  65. data/lib/object_present.rb +1 -1
  66. data/lib/option_value.rb +1 -1
  67. data/lib/resize_terminal.rb +1 -2
  68. data/lib/saved_assets.rb +1 -1
  69. data/lib/saved_files_matcher.rb +1 -1
  70. data/lib/shell_session.rb +439 -0
  71. data/lib/streams_out.rb +0 -1
  72. data/lib/string_util.rb +11 -1
  73. data/lib/success_result.rb +112 -0
  74. data/lib/text_analyzer.rb +1 -0
  75. data/lib/ww.rb +9 -7
  76. metadata +33 -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
@@ -395,10 +387,17 @@ module HashDelegatorSelf
395
387
  # @param [String] line The line to be processed.
396
388
  # @param [Array<Symbol>] selected_types A list of message types to check.
397
389
  # @param [Proc] block The block to be called with the line data.
398
- def yield_line_if_selected(line, selected_types, source_id: '', &block)
390
+ def yield_line_if_selected(line, selected_types, all_fcbs: nil,
391
+ source_id: '', &block)
399
392
  return unless block && block_type_selected?(selected_types, :line)
400
393
 
401
- block.call(:line, MarkdownExec::FCB.new(body: [line], id: source_id))
394
+ block.call(:line, persist_fcb_self(all_fcbs, body: [line], id: source_id))
395
+ end
396
+
397
+ def persist_fcb_self(all_fcbs, options)
398
+ fcb = MarkdownExec::FCB.new(options)
399
+ all_fcbs << fcb if all_fcbs
400
+ fcb
402
401
  end
403
402
  end
404
403
 
@@ -609,6 +608,7 @@ module MarkdownExec
609
608
  @dml_link_state = Struct.new(:document_filename, :inherited_lines)
610
609
  .new(@delegate_object[:filename], [])
611
610
  @dml_menu_blocks = []
611
+ @fcb_store = [] # all fcbs created
612
612
 
613
613
  @p_all_arguments = []
614
614
  @p_options_parsed = []
@@ -737,7 +737,7 @@ module MarkdownExec
737
737
 
738
738
  formatted_name = format(@delegate_object[:menu_link_format],
739
739
  HashDelegator.safeval(option_name))
740
- chrome_block = FCB.new(
740
+ chrome_block = persist_fcb(
741
741
  chrome: true,
742
742
  dname: HashDelegator.new(@delegate_object).string_send_color(
743
743
  formatted_name, :menu_chrome_color
@@ -781,7 +781,7 @@ module MarkdownExec
781
781
  chrome_blocks = link_state.inherited_lines_map do |line|
782
782
  formatted = format(@delegate_object[:menu_inherited_lines_format],
783
783
  { line: line })
784
- FCB.new(
784
+ persist_fcb(
785
785
  chrome: true,
786
786
  disabled: TtyMenu::DISABLE,
787
787
  dname: HashDelegator.new(@delegate_object).string_send_color(
@@ -840,25 +840,6 @@ module MarkdownExec
840
840
  end
841
841
  end
842
842
 
843
- # private
844
-
845
- def expand_references!(fcb, link_state)
846
- expand_variable_references!(
847
- blocks: [fcb],
848
- initial_code_required: false,
849
- link_state: link_state
850
- )
851
- expand_variable_references!(
852
- blocks: [fcb],
853
- echo_format: '%s',
854
- group_name: :command,
855
- initial_code_required: false,
856
- key_format: '$(%s)',
857
- link_state: link_state,
858
- pattern: options_command_substitution_regexp
859
- )
860
- end
861
-
862
843
  # Iterates through nested files to collect various types
863
844
  # of blocks, including dividers, tasks, and others.
864
845
  # The method categorizes blocks based on their type and processes them accordingly.
@@ -873,12 +854,15 @@ module MarkdownExec
873
854
 
874
855
  count = 0
875
856
  blocks = []
857
+ results = {}
876
858
  iter_blocks_from_nested_files do |btype, fcb|
877
859
  count += 1
878
860
  case btype
879
861
  when :blocks
862
+ result = SuccessResult.instance
880
863
  if @delegate_object[:bash]
881
- fcb.for_menu!(
864
+ # prepare block for menu, may fail and call HashDelegator.error_handler
865
+ result = fcb.for_menu!(
882
866
  block_calls_scan: @delegate_object[:block_calls_scan],
883
867
  block_name_match: @delegate_object[:block_name_match],
884
868
  block_name_nick_match: @delegate_object[:block_name_nick_match],
@@ -889,10 +873,11 @@ module MarkdownExec
889
873
  ) do |oname, color|
890
874
  apply_block_type_color_option(oname, color)
891
875
  end
876
+ results[fcb.id] = result if result.failure?
892
877
  else
893
878
  expand_references!(fcb, link_state)
894
879
  end
895
- blocks << fcb
880
+ blocks << fcb unless result.failure?
896
881
  when :filter # types accepted
897
882
  %i[blocks line]
898
883
  when :line
@@ -907,36 +892,12 @@ module MarkdownExec
907
892
  end
908
893
  end
909
894
  end
910
- # !!t blocks.count
911
- blocks
895
+ OpenStruct.new(blocks: blocks, results: results)
912
896
  rescue StandardError
897
+ # ww $@, $!,
913
898
  HashDelegator.error_handler('blocks_from_nested_files')
914
899
  end
915
900
 
916
- # find a block by its original (undecorated) name or nickname (not visible in menu)
917
- # if matched, the block returned has properties that it is from cli and not ui
918
- def block_state_for_name_from_cli(block_name)
919
- SelectedBlockMenuState.new(
920
- blocks_find_by_block_name(@dml_blocks_in_file, block_name),
921
- OpenStruct.new(
922
- block_name_from_cli: true,
923
- block_name_from_ui: false
924
- ),
925
- MenuState::CONTINUE
926
- )
927
- end
928
-
929
- def blocks_find_by_block_name(blocks, block_name)
930
- @dml_blocks_in_file.find do |item|
931
- # 2024-08-04 match oname for long block names
932
- # 2024-08-04 match nickname for long block names
933
- block_name == item.pub_name || block_name == item.nickname || block_name == item.oname
934
- end || @dml_menu_blocks.find do |item|
935
- # 2024-08-22 search in menu blocks to allow matching of automatic chrome with nickname
936
- block_name == item.pub_name || block_name == item.nickname || block_name == item.oname
937
- end
938
- end
939
-
940
901
  def build_menu_options(exit_option, display_mode_option,
941
902
  menu_entries, display_format)
942
903
  [exit_option,
@@ -949,7 +910,8 @@ module MarkdownExec
949
910
  initial_code_required: false, key_format:
950
911
  )
951
912
  evaluate_shell_expressions(
952
- (link_state&.inherited_lines_block || ''), commands,
913
+ (link_state&.inherited_lines_block || ''),
914
+ commands,
953
915
  initial_code_required: initial_code_required,
954
916
  key_format: key_format
955
917
  )
@@ -1025,6 +987,21 @@ module MarkdownExec
1025
987
  ]
1026
988
  end
1027
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
+
1028
1005
  # parse YAML body defining the UX for a single variable
1029
1006
  # set ENV value for the variable and return code lines for the same
1030
1007
  def code_from_ux_block_to_set_environment_variables(
@@ -1058,82 +1035,30 @@ module MarkdownExec
1058
1035
  code_lines.push "[[ -z $#{precondition} ]] && exit 1"
1059
1036
  end
1060
1037
 
1061
- exportable = true
1062
1038
  if only_default
1063
- value = case export.default
1064
- # exec > default
1065
- when :exec
1066
- raise unless export.exec.present?
1067
-
1068
- output = export_exec_with_code(
1069
- export, inherited_code, code_lines, required
1070
- )
1071
- if output == :invalidated
1072
- return :ux_exec_prohibited
1073
- end
1074
-
1075
- transform_export_value(output, export)
1076
- # default
1077
- else
1078
- export.default.to_s
1079
- end
1080
- else
1081
- value = nil
1082
-
1083
- # exec > allowed
1084
- if export.exec
1085
- value = export_exec_with_code(
1039
+ value, exportable, transformable =
1040
+ ux_block_export_automatic(
1086
1041
  export, inherited_code, code_lines, required
1087
1042
  )
1088
- if value == :invalidated
1089
- return :ux_exec_prohibited
1090
- end
1091
-
1092
- # allowed > prompt
1093
- elsif export.allowed && export.allowed.count.positive?
1094
- case (choice = prompt_select_code_filename(
1095
- [exit_prompt] + export.allowed,
1096
- string: export.prompt,
1097
- color_sym: :prompt_color_after_script_execution
1098
- ))
1099
- when exit_prompt
1100
- exportable = false
1101
- else
1102
- value = choice
1103
- end
1104
-
1105
- # prompt > default
1106
- elsif export.prompt.present?
1107
- begin
1108
- loop do
1109
- print "#{export.prompt} [#{export.default}]: "
1110
- value = gets.chomp
1111
- value = export.default.to_s if value.empty?
1112
- caps = NamedCaptureExtractor.extract_named_groups(value,
1113
- export.validate)
1114
- break if caps
1115
-
1116
- # invalid input, retry
1117
- end
1118
- rescue Interrupt
1119
- exportable = false
1120
- end
1043
+ else
1044
+ value, exportable, transformable =
1045
+ ux_block_export_activated(
1046
+ export, inherited_code, code_lines, required, exit_prompt
1047
+ )
1048
+ end
1049
+ return :ux_exec_prohibited if value == :ux_exec_prohibited
1121
1050
 
1122
- # default
1123
- else
1124
- value = export.default
1051
+ if SelectResponse.continue?(value)
1052
+ if transformable
1053
+ value = transform_export_value(value, export)
1125
1054
  end
1126
1055
 
1127
1056
  if exportable
1128
- value = transform_export_value(value, export)
1057
+ ENV[export.name] = value.to_s
1058
+ code_lines.push code_line_safe_assign(export.name, value,
1059
+ force: force)
1129
1060
  end
1130
1061
  end
1131
-
1132
- if exportable
1133
- ENV[export.name] = value.to_s
1134
- code_lines.push code_line_safe_assign(export.name, value,
1135
- force: force)
1136
- end
1137
1062
  else
1138
1063
  raise "Invalid data type: #{data.inspect}"
1139
1064
  end
@@ -1382,7 +1307,7 @@ module MarkdownExec
1382
1307
  next if exclude_types.include?(block.type)
1383
1308
 
1384
1309
  # Scan each block name for matches of the pattern
1385
- ([block.oname || ''] + block.body).join("\n").scan(pattern) do |(_, _variable_name)|
1310
+ count_named_group_occurrences_block_body_fix_indent(block).scan(pattern) do |(_, _variable_name)|
1386
1311
  pattern.match($LAST_MATCH_INFO.to_s) # Reapply match for named groups
1387
1312
  occurrence_count[$LAST_MATCH_INFO[group_name]] += 1
1388
1313
  end
@@ -1391,6 +1316,11 @@ module MarkdownExec
1391
1316
  occurrence_count
1392
1317
  end
1393
1318
 
1319
+ def count_named_group_occurrences_block_body_fix_indent(block)
1320
+ ### actually double the entries, but not a problem since it's used as a boolean
1321
+ ([block.oname || ''] + block.body).join("\n")
1322
+ end
1323
+
1394
1324
  ##
1395
1325
  # Creates and adds a formatted block to the blocks array
1396
1326
  # based on the provided match and format options.
@@ -1432,10 +1362,30 @@ module MarkdownExec
1432
1362
  line_cap[:line] ||= ''
1433
1363
 
1434
1364
  line_caps = [line_cap]
1435
- if wrap && line_cap[:text].length > screen_width_for_wrapping
1436
- wrapper = StringWrapper.new(width: screen_width_for_wrapping - line_cap[:indent].length)
1437
- line_caps = wrapper.wrap(line_cap[:text]).map do |wrapped_line|
1438
- line_cap.dup.merge(text: wrapped_line)
1365
+
1366
+ # split text with newlines, from variable expansion
1367
+ if line_cap[:text].include?("\n")
1368
+ lines = line_cap[:text].split("\n")
1369
+ line_caps = (lines.map do |line|
1370
+ line_cap.dup.merge(text: line)
1371
+ end.to_a)
1372
+ end
1373
+
1374
+ # wrap text on multiple lines to screen width, replacing line_caps
1375
+ if wrap
1376
+ line_caps = line_caps.flat_map do |line_cap|
1377
+ text = line_cap[:text]
1378
+ wrapper = StringWrapper.new(width: screen_width_for_wrapping - line_cap[:indent].length)
1379
+
1380
+ if text.length > screen_width_for_wrapping
1381
+ # Wrap this text and create line_cap objects for each part
1382
+ wrapper.wrap(text).map do |wrapped_text|
1383
+ line_cap.dup.merge(text: wrapped_text)
1384
+ end
1385
+ else
1386
+ # No change needed for this line
1387
+ line_cap
1388
+ end
1439
1389
  end
1440
1390
  end
1441
1391
 
@@ -1492,7 +1442,7 @@ module MarkdownExec
1492
1442
  fcb.type = type
1493
1443
  use_fcb = false # next line is new record
1494
1444
  else
1495
- fcb = FCB.new(
1445
+ fcb = persist_fcb(
1496
1446
  center: center,
1497
1447
  chrome: true,
1498
1448
  collapse: collapse.nil? ? (line_obj[:collapse] == COLLAPSIBLE_TOKEN_COLLAPSE) : collapse,
@@ -1534,7 +1484,9 @@ module MarkdownExec
1534
1484
  if block_given?
1535
1485
  # expand references only if block is recognized (not a comment)
1536
1486
  yield if block_given?
1537
- mbody = fcb.body[0].match @delegate_object[criteria[:match]]
1487
+
1488
+ # parse multiline to capture output of variable expansion
1489
+ mbody = fcb.body[0].match Regexp.new(@delegate_object[criteria[:match]], Regexp::MULTILINE)
1538
1490
  end
1539
1491
 
1540
1492
  create_and_add_chrome_block(
@@ -1579,7 +1531,7 @@ module MarkdownExec
1579
1531
  oname = format(@delegate_object[:menu_divider_format],
1580
1532
  HashDelegator.safeval(@delegate_object[divider_key]))
1581
1533
 
1582
- FCB.new(
1534
+ persist_fcb(
1583
1535
  chrome: true,
1584
1536
  disabled: TtyMenu::DISABLE,
1585
1537
  dname: string_send_color(oname, :menu_divider_color),
@@ -1989,7 +1941,7 @@ module MarkdownExec
1989
1941
  end
1990
1942
 
1991
1943
  def execute_block_in_state(block_name)
1992
- @dml_block_state = block_state_for_name_from_cli(block_name)
1944
+ @dml_block_state = find_block_state_by_name(block_name)
1993
1945
  dump_and_warn_block_state(name: block_name,
1994
1946
  selected: @dml_block_state.block)
1995
1947
  next_block_state =
@@ -2439,7 +2391,8 @@ module MarkdownExec
2439
2391
  # update blocks
2440
2392
  #
2441
2393
  Regexp.union(replacements.keys.map do |word|
2442
- Regexp.new(Regexp.escape(word))
2394
+ # match multiline text from variable expansion
2395
+ Regexp.new(Regexp.escape(word), Regexp::MULTILINE)
2443
2396
  end).tap do |pattern|
2444
2397
  menu_blocks.each do |block|
2445
2398
  next if exclude_types.include?(block.type)
@@ -2449,9 +2402,26 @@ module MarkdownExec
2449
2402
  end
2450
2403
  end
2451
2404
 
2405
+ def expand_references!(fcb, link_state)
2406
+ expand_variable_references!(
2407
+ blocks: [fcb],
2408
+ initial_code_required: false,
2409
+ link_state: link_state
2410
+ )
2411
+ expand_variable_references!(
2412
+ blocks: [fcb],
2413
+ echo_format: '%s',
2414
+ group_name: :command,
2415
+ initial_code_required: false,
2416
+ key_format: '$(%s)',
2417
+ link_state: link_state,
2418
+ pattern: options_command_substitution_regexp
2419
+ )
2420
+ end
2421
+
2452
2422
  def expand_variable_references!(
2453
2423
  blocks:,
2454
- echo_format: 'echo $%s',
2424
+ echo_format: 'echo "$%s"',
2455
2425
  group_name: :variable,
2456
2426
  initial_code_required: false,
2457
2427
  key_format: '${%s}',
@@ -2479,6 +2449,40 @@ module MarkdownExec
2479
2449
  expand_blocks_with_replacements(blocks, replacements)
2480
2450
  end
2481
2451
 
2452
+ def export_echo_with_code_single(export_echo, inherited_code, code_lines,
2453
+ required)
2454
+ code = %(printf '%s' "#{export_echo}")
2455
+ value = execute_temporary_script(
2456
+ code,
2457
+ (inherited_code || []) +
2458
+ code_lines + required[:code]
2459
+ )
2460
+ if value == :invalidated
2461
+ warn "A value must exist for: #{export.preconditions.join(', ')}"
2462
+ end
2463
+ value
2464
+ end
2465
+
2466
+ def export_echo_with_code(export, inherited_code, code_lines, required,
2467
+ force:)
2468
+ exportable = true
2469
+ case export.echo
2470
+ when String
2471
+ value = export_echo_with_code_single(export.echo, inherited_code,
2472
+ code_lines, required)
2473
+ when Hash
2474
+ # each item in the hash is a variable name and value
2475
+ export.echo.each do |name, expression|
2476
+ value = export_echo_with_code_single(expression, inherited_code,
2477
+ code_lines, required)
2478
+ ENV[name] = value.to_s
2479
+ code_lines.push code_line_safe_assign(name, value, force: force)
2480
+ end
2481
+ exportable = false
2482
+ end
2483
+ [value, exportable]
2484
+ end
2485
+
2482
2486
  def export_exec_with_code(export, inherited_code, code_lines, required)
2483
2487
  value = execute_temporary_script(
2484
2488
  export.exec,
@@ -2524,6 +2528,31 @@ module MarkdownExec
2524
2528
  { size: file_size, lines: line_count }
2525
2529
  end
2526
2530
 
2531
+ # Search in @dml_blocks_in_file first,
2532
+ # fallback to @dml_menu_blocks if not found.
2533
+ def find_block_by_name(blocks, block_name)
2534
+ match_block = ->(item) do
2535
+ [item.pub_name, item.nickname,
2536
+ item.oname, item.s2title].include?(block_name)
2537
+ end
2538
+
2539
+ @dml_blocks_in_file.find(&match_block) ||
2540
+ @dml_menu_blocks.find(&match_block)
2541
+ end
2542
+
2543
+ # find a block by its original (undecorated) name or nickname (not visible in menu)
2544
+ # if matched, the block returned has properties that it is from cli and not ui
2545
+ def find_block_state_by_name(block_name)
2546
+ SelectedBlockMenuState.new(
2547
+ find_block_by_name(@dml_blocks_in_file, block_name),
2548
+ OpenStruct.new(
2549
+ block_name_from_cli: true,
2550
+ block_name_from_ui: false
2551
+ ),
2552
+ MenuState::CONTINUE
2553
+ )
2554
+ end
2555
+
2527
2556
  def format_and_execute_command(
2528
2557
  code_lines:,
2529
2558
  erls:,
@@ -2532,6 +2561,7 @@ module MarkdownExec
2532
2561
  formatted_command = code_lines.flatten.join("\n")
2533
2562
  @fout.fout fetch_color(data_sym: :script_execution_head,
2534
2563
  color_sym: :script_execution_frame_color)
2564
+
2535
2565
  command_execute(
2536
2566
  formatted_command,
2537
2567
  args: @pass_args,
@@ -2641,10 +2671,8 @@ module MarkdownExec
2641
2671
  @process_mutex.synchronize do
2642
2672
  Thread.new do
2643
2673
  stream.each_line do |line|
2644
- line.strip!
2645
2674
  if @run_state.files.streams
2646
- @run_state.files.append_stream_line(file_type,
2647
- line)
2675
+ @run_state.files.append_stream_line(file_type, line)
2648
2676
  end
2649
2677
 
2650
2678
  puts line if @delegate_object[:output_stdout]
@@ -2692,7 +2720,7 @@ module MarkdownExec
2692
2720
  Regexp.new(@delegate_object.fetch(
2693
2721
  :fenced_start_and_end_regex, '^(?<indent> *)`{3,}'
2694
2722
  )),
2695
- fcb: MarkdownExec::FCB.new(id: 'INIT'),
2723
+ fcb: persist_fcb(id: 'INIT'),
2696
2724
  in_fenced_block: false,
2697
2725
  headings: []
2698
2726
  }
@@ -2717,7 +2745,7 @@ module MarkdownExec
2717
2745
  menu_entries, current_display_format
2718
2746
  )
2719
2747
 
2720
- selection = prompt_select_code_filename(
2748
+ selection = prompt_select_from_list(
2721
2749
  menu_options,
2722
2750
  string: menu_title,
2723
2751
  color_sym: :prompt_color_after_script_execution
@@ -2773,7 +2801,7 @@ module MarkdownExec
2773
2801
  def iter_source_blocks(source, source_id: nil, &block)
2774
2802
  case source
2775
2803
  when 1
2776
- blocks_from_nested_files(source_id: source_id).each(&block)
2804
+ blocks_from_nested_files(source_id: source_id).blocks.each(&block)
2777
2805
  when 2
2778
2806
  @dml_blocks_in_file.each(&block)
2779
2807
  when 3
@@ -2816,7 +2844,8 @@ module MarkdownExec
2816
2844
  nil),
2817
2845
  end_pattern: @delegate_object.fetch(:output_assignment_end, nil),
2818
2846
  scan1: @delegate_object.fetch(:output_assignment_match, nil),
2819
- format1: @delegate_object.fetch(:output_assignment_format, nil)
2847
+ format1: @delegate_object.fetch(:output_assignment_format, nil),
2848
+ name: ''
2820
2849
  )
2821
2850
 
2822
2851
  else
@@ -3077,7 +3106,7 @@ module MarkdownExec
3077
3106
  else
3078
3107
  ## user selects from existing files or other
3079
3108
  #
3080
- case (name = prompt_select_code_filename(
3109
+ case (name = prompt_select_from_list(
3081
3110
  [@delegate_object[:prompt_filespec_back]] + files,
3082
3111
  string: @delegate_object[:prompt_select_code_file],
3083
3112
  color_sym: :prompt_color_after_script_execution
@@ -3108,11 +3137,19 @@ module MarkdownExec
3108
3137
  end
3109
3138
 
3110
3139
  def mdoc_and_blocks_from_nested_files(source_id: nil)
3111
- menu_blocks = blocks_from_nested_files(source_id: source_id)
3112
- mdoc = MDoc.new(menu_blocks) do |nopts|
3140
+ blocks_results = blocks_from_nested_files(source_id: source_id)
3141
+
3142
+ blocks_results.results.select do |_id, result|
3143
+ result.failure?
3144
+ end.each do |id, result|
3145
+ HashDelegator.error_handler("#{id} - #{result.to_yaml}")
3146
+ end
3147
+
3148
+ mdoc = MDoc.new(blocks_results.blocks) do |nopts|
3113
3149
  @delegate_object.merge!(nopts)
3114
3150
  end
3115
- [menu_blocks, mdoc]
3151
+
3152
+ [blocks_results.blocks, mdoc]
3116
3153
  end
3117
3154
 
3118
3155
  ## Handles the file loading and returns the blocks
@@ -3180,7 +3217,7 @@ module MarkdownExec
3180
3217
  #
3181
3218
  menu_blocks.each do |fcb|
3182
3219
  fcb.body = fcb.raw_body || fcb.body || []
3183
- fcb.dname = fcb.raw_dname || fcb.dname
3220
+ fcb.name_in_menu!(fcb.raw_dname || fcb.dname)
3184
3221
  fcb.s0printable = fcb.raw_s0printable || fcb.s0printable
3185
3222
  fcb.s1decorated = fcb.raw_s1decorated || fcb.s1decorated
3186
3223
  expand_references!(fcb, link_state)
@@ -3212,7 +3249,7 @@ module MarkdownExec
3212
3249
  #
3213
3250
  return unless block.nil?
3214
3251
 
3215
- chrome_block = FCB.new(
3252
+ chrome_block = persist_fcb(
3216
3253
  chrome: true,
3217
3254
  disabled: TtyMenu::DISABLE,
3218
3255
  dname: HashDelegator.new(@delegate_object).string_send_color(
@@ -3259,6 +3296,19 @@ module MarkdownExec
3259
3296
  end
3260
3297
  end
3261
3298
 
3299
+ def menu_from_list_with_back(list)
3300
+ case (name = prompt_select_from_list(
3301
+ [@delegate_object[:prompt_filespec_back]] + list,
3302
+ string: @delegate_object[:prompt_select_code_file],
3303
+ color_sym: :prompt_color_after_script_execution
3304
+ ))
3305
+ when @delegate_object[:prompt_filespec_back]
3306
+ SelectResponse::BACK
3307
+ else
3308
+ name
3309
+ end
3310
+ end
3311
+
3262
3312
  def menu_toggle_collapsible_block(selected)
3263
3313
  # return true if @compress_ids.key?(fcb.id) && !!@compress_ids[fcb.id]
3264
3314
  # return false if @expand_ids.key?(fcb.id) && !!@expand_ids[fcb.id]
@@ -3371,6 +3421,12 @@ module MarkdownExec
3371
3421
  prompt_select_continue == MenuState::EXIT
3372
3422
  end
3373
3423
 
3424
+ def persist_fcb(options)
3425
+ MarkdownExec::FCB.new(options).tap do |fcb|
3426
+ @fcb_store << fcb
3427
+ end
3428
+ end
3429
+
3374
3430
  def pop_add_current_code_to_head_and_trigger_load(
3375
3431
  link_state, block_names, code_lines,
3376
3432
  dependencies, selected, next_block_name: nil
@@ -3486,7 +3542,7 @@ module MarkdownExec
3486
3542
  # private
3487
3543
 
3488
3544
  def process_string_array(arr, begin_pattern: nil, end_pattern: nil, scan1: nil,
3489
- format1: nil)
3545
+ format1: nil, name: '')
3490
3546
  in_block = !begin_pattern.present?
3491
3547
  collected_lines = []
3492
3548
 
@@ -3508,6 +3564,9 @@ module MarkdownExec
3508
3564
  collected_lines << formatted
3509
3565
  end
3510
3566
  end
3567
+ elsif format1.present?
3568
+ formatted = format(format1, { value: line })
3569
+ collected_lines << formatted
3511
3570
  else
3512
3571
  collected_lines << line
3513
3572
  end
@@ -3633,9 +3692,22 @@ module MarkdownExec
3633
3692
  0
3634
3693
  end
3635
3694
 
3695
+ def prompt_select_continue(filter: true, quiet: true)
3696
+ sel = @prompt.select(
3697
+ string_send_color(@delegate_object[:prompt_after_script_execution],
3698
+ :prompt_color_after_script_execution),
3699
+ filter: filter,
3700
+ quiet: quiet
3701
+ ) do |menu|
3702
+ menu.choice @delegate_object[:prompt_yes]
3703
+ menu.choice @delegate_object[:prompt_exit]
3704
+ end
3705
+ sel == @delegate_object[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
3706
+ end
3707
+
3636
3708
  # public
3637
3709
 
3638
- def prompt_select_code_filename(
3710
+ def prompt_select_from_list(
3639
3711
  filenames,
3640
3712
  color_sym: :prompt_color_after_script_execution,
3641
3713
  cycle: true,
@@ -3661,19 +3733,6 @@ module MarkdownExec
3661
3733
  end
3662
3734
  end
3663
3735
 
3664
- def prompt_select_continue(filter: true, quiet: true)
3665
- sel = @prompt.select(
3666
- string_send_color(@delegate_object[:prompt_after_script_execution],
3667
- :prompt_color_after_script_execution),
3668
- filter: filter,
3669
- quiet: quiet
3670
- ) do |menu|
3671
- menu.choice @delegate_object[:prompt_yes]
3672
- menu.choice @delegate_object[:prompt_exit]
3673
- end
3674
- sel == @delegate_object[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
3675
- end
3676
-
3677
3736
  # user prompt to exit if the menu will be displayed again
3678
3737
  #
3679
3738
  def prompt_user_exit(block_name_from_cli:, selected:)
@@ -3747,27 +3806,6 @@ module MarkdownExec
3747
3806
  end&.compact
3748
3807
  end
3749
3808
 
3750
- def saved_asset_for_history(
3751
- file:, form:, match_info:
3752
- )
3753
- OpenStruct.new(
3754
- file: file[(Dir.pwd.length + 1)..-1],
3755
- full: file,
3756
- row: format(
3757
- form,
3758
- # default '*' so unknown parameters are given a wildcard
3759
- match_info.names.each_with_object(Hash.new('*')) do |name, hash|
3760
- hash[name.to_sym] = match_info[name]
3761
- end
3762
- )
3763
- )
3764
- rescue KeyError
3765
- # pp $!, $@
3766
- warn "Cannot format with: #{@delegate_object[:saved_history_format]}"
3767
- error_handler('saved_history_format')
3768
- :break
3769
- end
3770
-
3771
3809
  # Processes YAML data from the selected menu item, updating delegate
3772
3810
  # objects and optionally printing formatted output.
3773
3811
  # @param selected [Hash] Selected item from the menu containing a YAML body.
@@ -3822,8 +3860,8 @@ module MarkdownExec
3822
3860
  # console [height, width]. If not provided or if the terminal
3823
3861
  # is resized, it will be set to the current console dimensions.
3824
3862
  # - :select_page_height [Integer, nil] The height of the page for
3825
- # selection. If not provided or if not positive, it will be set
3826
- # to the maximum of (console height - 3) or 4.
3863
+ # selection. If not provided or if not positive, it
3864
+ # will be set to the maximum of (console height - 3) or 4.
3827
3865
  # - :per_page [Integer, nil] The number of items per page. If
3828
3866
  # :select_page_height is not provided or if not positive, it
3829
3867
  # will be set to the maximum of (console height - 3) or 4.
@@ -3950,7 +3988,7 @@ module MarkdownExec
3950
3988
  ## user selects from existing files or other
3951
3989
  # input into path with wildcard for easy entry
3952
3990
  #
3953
- case (name = prompt_select_code_filename(
3991
+ case (name = prompt_select_from_list(
3954
3992
  [@delegate_object[:prompt_filespec_back],
3955
3993
  @delegate_object[:prompt_filespec_other]] + files,
3956
3994
  string: @delegate_object[:prompt_select_code_file],
@@ -3987,6 +4025,27 @@ module MarkdownExec
3987
4025
  ).generate_name
3988
4026
  end
3989
4027
 
4028
+ def saved_asset_for_history(
4029
+ file:, form:, match_info:
4030
+ )
4031
+ OpenStruct.new(
4032
+ file: file[(Dir.pwd.length + 1)..-1],
4033
+ full: file,
4034
+ row: format(
4035
+ form,
4036
+ # default '*' so unknown parameters are given a wildcard
4037
+ match_info.names.each_with_object(Hash.new('*')) do |name, hash|
4038
+ hash[name.to_sym] = match_info[name]
4039
+ end
4040
+ )
4041
+ )
4042
+ rescue KeyError
4043
+ # pp $!, $@
4044
+ warn "Cannot format with: #{@delegate_object[:saved_history_format]}"
4045
+ error_handler('saved_history_format')
4046
+ :break
4047
+ end
4048
+
3990
4049
  def screen_width
3991
4050
  width = @delegate_object[:screen_width]
3992
4051
  if width&.positive?
@@ -4195,7 +4254,7 @@ module MarkdownExec
4195
4254
  TtyMenu::ENABLE
4196
4255
  end
4197
4256
 
4198
- MarkdownExec::FCB.new(
4257
+ persist_fcb(
4199
4258
  body: [],
4200
4259
  call: rest.match(
4201
4260
  Regexp.new(@delegate_object[:block_calls_scan])
@@ -4320,7 +4379,9 @@ module MarkdownExec
4320
4379
  # add line if it is depth 0 or option allows it
4321
4380
  #
4322
4381
  HashDelegator.yield_line_if_selected(
4323
- line, selected_types, source_id: source_id, &block
4382
+ line, selected_types,
4383
+ all_fcbs: @fcb_store,
4384
+ source_id: source_id, &block
4324
4385
  )
4325
4386
  end
4326
4387
  end
@@ -4333,6 +4394,147 @@ module MarkdownExec
4333
4394
  @delegate_object.merge!(options)
4334
4395
  end
4335
4396
 
4397
+ def ux_block_export_activated(export, inherited_code, code_lines, required,
4398
+ exit_prompt)
4399
+ exportable = true
4400
+ transformable = true
4401
+ [if export.allowed.present?
4402
+ if export.allowed == :echo
4403
+ output, exportable = export_echo_with_code(
4404
+ export, inherited_code, code_lines, required, force: true
4405
+ )
4406
+ return :ux_exec_prohibited if output == :invalidated
4407
+
4408
+ menu_from_list_with_back(output.split("\n"))
4409
+
4410
+ elsif export.allowed == :exec
4411
+ output = process_allowed_exec(export, inherited_code,
4412
+ code_lines, required)
4413
+ return :ux_exec_prohibited if output == :ux_exec_prohibited
4414
+
4415
+ menu_from_list_with_back(output)
4416
+
4417
+ else
4418
+ menu_from_list_with_back(export.allowed)
4419
+ end
4420
+
4421
+ # echo > exec
4422
+ elsif export.echo
4423
+ output, exportable = export_echo_with_code(
4424
+ export, inherited_code, code_lines, required, force: true
4425
+ )
4426
+ return :ux_exec_prohibited if output == :invalidated
4427
+
4428
+ output
4429
+
4430
+ # exec > allowed
4431
+ elsif export.exec
4432
+ output = export_exec_with_code(
4433
+ export, inherited_code, code_lines, required
4434
+ )
4435
+ return :ux_exec_prohibited if output == :invalidated
4436
+
4437
+ output
4438
+
4439
+ # allowed > prompt
4440
+ elsif export.allowed && export.allowed.count.positive?
4441
+ case (choice = prompt_select_from_list(
4442
+ [exit_prompt] + export.allowed,
4443
+ string: export.prompt,
4444
+ color_sym: :prompt_color_after_script_execution
4445
+ ))
4446
+ when exit_prompt
4447
+ exportable = false
4448
+ transformable = false
4449
+ nil
4450
+ else
4451
+ choice
4452
+ end
4453
+
4454
+ # prompt > default
4455
+ elsif export.prompt.present?
4456
+ begin
4457
+ loop do
4458
+ print "#{export.prompt} [#{export.default}]: "
4459
+ output = gets.chomp
4460
+ output = export.default.to_s if output.empty?
4461
+ caps = NamedCaptureExtractor.extract_named_groups(output,
4462
+ export.validate)
4463
+ break if caps
4464
+
4465
+ # invalid input, retry
4466
+ end
4467
+ rescue Interrupt
4468
+ exportable = false
4469
+ transformable = false
4470
+ end
4471
+
4472
+ output
4473
+
4474
+ # default
4475
+ else
4476
+ transformable = false
4477
+ export.default
4478
+ end, exportable, transformable]
4479
+ end
4480
+
4481
+ def ux_block_export_automatic(export, inherited_code, code_lines, required)
4482
+ transformable = true
4483
+ exportable = true
4484
+ [case export.default
4485
+ when :allowed
4486
+ raise unless export.allowed.present?
4487
+
4488
+ if export.allowed == :echo
4489
+ output, exportable = export_echo_with_code(
4490
+ export, inherited_code, code_lines, required, force: false
4491
+ )
4492
+ return :ux_exec_prohibited if output == :invalidated
4493
+
4494
+ exportable && output.split("\n").first
4495
+
4496
+ elsif export.allowed == :exec
4497
+ output = process_allowed_exec(export, inherited_code,
4498
+ code_lines, required)
4499
+ return :ux_exec_prohibited if output == :ux_exec_prohibited
4500
+
4501
+ output.first
4502
+
4503
+ else
4504
+ export.allowed.first
4505
+ end
4506
+
4507
+ # echo > default
4508
+ when :echo
4509
+ raise unless export.echo.present?
4510
+
4511
+ output, exportable = export_echo_with_code(
4512
+ export, inherited_code, code_lines, required, force: false
4513
+ )
4514
+ return :ux_exec_prohibited if output == :invalidated
4515
+
4516
+ output
4517
+
4518
+ # exec > default
4519
+ when :exec
4520
+ raise unless export.exec.present?
4521
+
4522
+ output = export_exec_with_code(
4523
+ export, inherited_code, code_lines, required
4524
+ )
4525
+ return :ux_exec_prohibited if output == :invalidated
4526
+
4527
+ output
4528
+
4529
+ # default
4530
+ else
4531
+ transformable = false
4532
+ export.default.to_s
4533
+ end,
4534
+ exportable,
4535
+ transformable]
4536
+ end
4537
+
4336
4538
  def vux_await_user_selection(prior_answer: @dml_block_selection)
4337
4539
  @dml_block_state = load_cli_or_user_selected_block(
4338
4540
  all_blocks: @dml_blocks_in_file,
@@ -4359,7 +4561,7 @@ module MarkdownExec
4359
4561
  end
4360
4562
 
4361
4563
  def vux_execute_and_prompt(block_name)
4362
- @dml_block_state = block_state_for_name_from_cli(block_name)
4564
+ @dml_block_state = find_block_state_by_name(block_name)
4363
4565
  if @dml_block_state.block &&
4364
4566
  @dml_block_state.block.type == BlockType::OPTS
4365
4567
  debounce_reset
@@ -4532,7 +4734,8 @@ module MarkdownExec
4532
4734
 
4533
4735
  inherited_block_names = []
4534
4736
  inherited_dependencies = {}
4535
- selected = FCB.new(oname: 'load_code')
4737
+ selected = persist_fcb(oname: 'load_code')
4738
+
4536
4739
  pop_add_current_code_to_head_and_trigger_load(
4537
4740
  @dml_link_state, inherited_block_names,
4538
4741
  code_lines, inherited_dependencies, selected
@@ -4800,7 +5003,7 @@ module MarkdownExec
4800
5003
  def vux_user_selected_block_name
4801
5004
  if @dml_link_state.block_name.present?
4802
5005
  # @prior_block_was_link = true
4803
- @dml_block_state.block = blocks_find_by_block_name(
5006
+ @dml_block_state.block = find_block_by_name(
4804
5007
  @dml_blocks_in_file,
4805
5008
  @dml_link_state.block_name
4806
5009
  )
@@ -5269,7 +5472,7 @@ module MarkdownExec
5269
5472
  end
5270
5473
 
5271
5474
  def test_blocks_from_nested_files
5272
- result = @hd.blocks_from_nested_files
5475
+ result = @hd.blocks_from_nested_files.blocks
5273
5476
  assert_kind_of Array, result
5274
5477
  assert_kind_of FCB, result.first
5275
5478
  end
@@ -5278,7 +5481,7 @@ module MarkdownExec
5278
5481
  @hd = HashDelegator.new(no_chrome: true)
5279
5482
  @hd.expects(:create_and_add_chrome_blocks).never
5280
5483
 
5281
- result = @hd.blocks_from_nested_files
5484
+ result = @hd.blocks_from_nested_files.blocks
5282
5485
 
5283
5486
  assert_kind_of Array, result
5284
5487
  end
@@ -5724,7 +5927,7 @@ module MarkdownExec
5724
5927
  Thread.new { @hd.handle_stream(stream: stream, file_type: file_type) }
5725
5928
 
5726
5929
  @hd.wait_for_stream_processing
5727
- assert_equal ['line 1', 'line 2'],
5930
+ assert_equal ["line 1\n", "line 2\n"],
5728
5931
  @hd.instance_variable_get(:@run_state)
5729
5932
  .files.stream_lines(ExecutionStreams::STD_OUT)
5730
5933
  end