markdown_exec 2.5.0 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/Gemfile.lock +2 -2
  4. data/Rakefile +3 -3
  5. data/bats/block-types.bats +13 -7
  6. data/bats/import.bats +6 -0
  7. data/bats/markup.bats +6 -15
  8. data/bats/options-collapse.bats +26 -0
  9. data/bats/options.bats +1 -1
  10. data/bats/table.bats +8 -0
  11. data/bats/test_helper.bash +74 -49
  12. data/bats/variable-expansion.bats +46 -0
  13. data/bin/tab_completion.sh +1 -1
  14. data/docs/dev/bats-document-configuration.md +8 -1
  15. data/docs/dev/block-type-bash.md +1 -1
  16. data/docs/dev/block-type-opts.md +1 -5
  17. data/docs/dev/block-type-vars.md +4 -0
  18. data/docs/dev/import-missing.md +2 -0
  19. data/docs/dev/menu-cli.md +1 -1
  20. data/docs/dev/options-collapse.md +47 -0
  21. data/docs/dev/requiring-blocks.md +3 -0
  22. data/docs/dev/specs.md +2 -1
  23. data/docs/dev/table-crash.md +39 -0
  24. data/docs/dev/table-indent.md +26 -0
  25. data/docs/dev/text-decoration.md +2 -5
  26. data/docs/dev/variable-expansion.md +2 -4
  27. data/examples/bash-blocks.md +1 -1
  28. data/examples/block-names.md +1 -1
  29. data/examples/block-types.md +1 -1
  30. data/examples/data-files.md +1 -1
  31. data/examples/document_options.md +2 -2
  32. data/examples/indent.md +1 -1
  33. data/examples/interrupt.md +1 -1
  34. data/examples/link-blocks-vars.md +1 -1
  35. data/examples/linked.md +1 -1
  36. data/examples/linked1.md +1 -1
  37. data/examples/nickname.md +1 -1
  38. data/examples/opts-blocks-require.md +1 -1
  39. data/examples/opts-blocks.md +1 -1
  40. data/examples/opts_output_execution.md +1 -1
  41. data/examples/pass-through-arguments.md +1 -1
  42. data/examples/pause-after-execution.md +1 -1
  43. data/examples/port-blocks.md +1 -1
  44. data/examples/save.md +1 -1
  45. data/examples/text-markup.md +1 -1
  46. data/examples/variable-expansion.md +6 -2
  47. data/examples/vars-blocks.md +1 -1
  48. data/examples/wrap.md +1 -1
  49. data/lib/block_types.rb +4 -0
  50. data/lib/cached_nested_file_reader.rb +7 -4
  51. data/lib/collapser.rb +302 -0
  52. data/lib/constants.rb +10 -0
  53. data/lib/evaluate_shell_expressions.rb +0 -3
  54. data/lib/fcb.rb +13 -17
  55. data/lib/format_table.rb +11 -7
  56. data/lib/hash_delegator.rb +461 -272
  57. data/lib/hierarchy_string.rb +5 -1
  58. data/lib/markdown_exec/version.rb +1 -1
  59. data/lib/markdown_exec.rb +16 -32
  60. data/lib/mdoc.rb +100 -35
  61. data/lib/menu.src.yml +124 -17
  62. data/lib/menu.yml +102 -16
  63. data/lib/ww.rb +75 -22
  64. metadata +12 -9
  65. data/lib/append_to_bash_history.rb +0 -303
  66. data/lib/ce_get_cost_and_usage.rb +0 -23
  67. data/lib/doh.rb +0 -190
  68. data/lib/layered_hash.rb +0 -143
  69. data/lib/poly.rb +0 -171
@@ -20,7 +20,6 @@ require 'yaml'
20
20
  require_relative 'ansi_string'
21
21
  require_relative 'array'
22
22
  require_relative 'array_util'
23
- # require_relative 'block_label'
24
23
  require_relative 'block_types'
25
24
  require_relative 'cached_nested_file_reader'
26
25
  require_relative 'constants'
@@ -165,6 +164,7 @@ module HashDelegatorSelf
165
164
 
166
165
  def initialize_fcb_names(fcb)
167
166
  fcb.oname = fcb.dname = fcb.title || ''
167
+ fcb.s2title = fcb.oname
168
168
  end
169
169
 
170
170
  def join_code_lines(lines)
@@ -267,6 +267,7 @@ module HashDelegatorSelf
267
267
  def tables_into_columns!(blocks_menu, delegate_object)
268
268
  return unless delegate_object[:tables_into_columns]
269
269
 
270
+ screenwidth = delegate_object[:console_width]
270
271
  lines = blocks_menu.map(&:oname)
271
272
  text_tables = TableExtractor.extract_tables(
272
273
  lines,
@@ -279,7 +280,7 @@ module HashDelegatorSelf
279
280
 
280
281
  range = table[:start_index]..(table[:start_index] + table[:rows] - 1)
281
282
  lines = blocks_menu[range].map(&:dname)
282
- formatted = MarkdownTableFormatter.format_table(
283
+ table__hs = MarkdownTableFormatter.format_table__hs(
283
284
  column_count: table[:columns],
284
285
  decorate: {
285
286
  border: delegate_object[:table_border_color],
@@ -290,8 +291,7 @@ module HashDelegatorSelf
290
291
  lines: lines
291
292
  )
292
293
 
293
- unless formatted.count == range.size
294
- # warn [__LINE__, range, lines, formatted].inspect
294
+ unless table__hs.count == range.size
295
295
  raise 'Invalid result from MarkdownTableFormatter.format_table()'
296
296
  end
297
297
 
@@ -300,11 +300,28 @@ module HashDelegatorSelf
300
300
 
301
301
  # replace text in each block
302
302
  range.each.with_index do |block_ind, ind|
303
- ### format oname to dname
304
- blocks_menu[block_ind].dname = indent + formatted[ind]
303
+ fcb = blocks_menu[block_ind]
304
+ fcb.s3formatted_table_row = fcb.padded = table__hs[ind] ####
305
+ fcb.padded_width = table__hs[ind].padded_width
306
+ if fcb.center
307
+ cw = (screenwidth - table__hs[ind].padded_width) / 2
308
+ if cw.positive?
309
+ indent = ' ' * cw
310
+ fcb.s3indent = fcb.indent = indent
311
+ end
312
+ else
313
+ fcb.s3indent = fcb.indent
314
+ end
315
+ fcb.s3indent ||= ''
316
+ fcb.dname = fcb.indented_decorated = fcb.s3indent + fcb.s3formatted_table_row.decorate
305
317
  end
306
318
  end
307
319
  end
320
+ # s0indent: indent,
321
+ # s0printable: line_obj[:text],
322
+ # s1decorated: decorated,
323
+ # s2title = fcb.oname
324
+ # s3formatted_table_row = fcb.padded = table__hs[ind]####
308
325
 
309
326
  # Creates a TTY prompt with custom settings. Specifically,
310
327
  # it disables the default 'cross' symbol and
@@ -345,10 +362,10 @@ module HashDelegatorSelf
345
362
  # @param [String] line The line to be processed.
346
363
  # @param [Array<Symbol>] selected_types A list of message types to check.
347
364
  # @param [Proc] block The block to be called with the line data.
348
- def yield_line_if_selected(line, selected_types, &block)
365
+ def yield_line_if_selected(line, selected_types, id: '', &block)
349
366
  return unless block && block_type_selected?(selected_types, :line)
350
367
 
351
- block.call(:line, MarkdownExec::FCB.new(body: [line]))
368
+ block.call(:line, MarkdownExec::FCB.new(body: [line], id: id))
352
369
  end
353
370
  end
354
371
 
@@ -532,7 +549,7 @@ module MarkdownExec
532
549
  end
533
550
 
534
551
  class HashDelegatorParent
535
- attr_accessor :most_recent_loaded_filename, :pass_args, :run_state,
552
+ attr_accessor :pass_args, :run_state,
536
553
  :p_all_arguments, :p_options_parsed, :p_params, :p_rest
537
554
 
538
555
  extend HashDelegatorSelf
@@ -543,7 +560,8 @@ module MarkdownExec
543
560
  @delegate_object = delegate_object
544
561
  @prompt = HashDelegator.tty_prompt_without_disabled_symbol
545
562
 
546
- @most_recent_loaded_filename = nil
563
+ @opts_most_recent_filename = nil
564
+ @vars_most_recent_filename = nil
547
565
  @pass_args = []
548
566
  @run_state = OpenStruct.new(
549
567
  link_history: [],
@@ -559,17 +577,9 @@ module MarkdownExec
559
577
  @p_options_parsed = []
560
578
  @p_params = {}
561
579
  @p_rest = []
562
- end
563
580
 
564
- # private
565
-
566
- # def [](key)
567
- # @delegate_object[key]
568
- # end
569
-
570
- # def []=(key, value)
571
- # @delegate_object[key] = value
572
- # end
581
+ @compressed_ids = nil
582
+ end
573
583
 
574
584
  ##
575
585
  # Returns the absolute path of the given file path.
@@ -591,11 +601,25 @@ module MarkdownExec
591
601
  end
592
602
  end
593
603
 
604
+ def add_back_option(id: '', menu_blocks:)
605
+ append_chrome_block(id: id, menu_blocks: menu_blocks,
606
+ menu_state: MenuState::BACK)
607
+ end
608
+
609
+ def add_exit_option(id: '', menu_blocks:)
610
+ append_chrome_block(id: id, menu_blocks: menu_blocks,
611
+ menu_state: MenuState::EXIT)
612
+ end
613
+
614
+ def add_inherited_lines(menu_blocks:, link_state:)
615
+ append_inherited_lines(menu_blocks: menu_blocks, link_state: link_state)
616
+ end
617
+
594
618
  # Modifies the provided menu blocks array by adding 'Back' and 'Exit' options,
595
619
  # along with initial and final dividers, based on the delegate object's configuration.
596
620
  #
597
621
  # @param menu_blocks [Array] The array of menu block elements to be modified.
598
- def add_menu_chrome_blocks!(menu_blocks:, link_state:)
622
+ def add_menu_chrome_blocks!(id: '', menu_blocks:, link_state:)
599
623
  return unless @delegate_object[:menu_link_format].present?
600
624
 
601
625
  if @delegate_object[:menu_with_inherited_lines]
@@ -604,89 +628,18 @@ module MarkdownExec
604
628
  end
605
629
 
606
630
  # back before exit
607
- add_back_option(menu_blocks: menu_blocks) if should_add_back_option?
631
+ add_back_option(id: "#{id}.back",
632
+ menu_blocks: menu_blocks) if should_add_back_option?
608
633
 
609
634
  # exit after other options
610
635
  if @delegate_object[:menu_with_exit]
611
- add_exit_option(menu_blocks: menu_blocks)
636
+ add_exit_option(id: "#{id}.exit", menu_blocks: menu_blocks)
612
637
  end
613
638
 
614
- append_divider(menu_blocks: menu_blocks, position: :initial)
615
- append_divider(menu_blocks: menu_blocks, position: :final)
616
- end
617
-
618
- def variable_expansions!(
619
- echo_command_form: 'echo "$%s"',
620
- link_state:,
621
- menu_blocks:,
622
- regexp: Regexp.new(@delegate_object[:variable_expression_regexp])
623
- )
624
- # !!v link_state.inherited_lines_block
625
- # collect variables in menu_blocks
626
- #
627
- variables_count = Hash.new(0)
628
- menu_blocks.each do |fcb|
629
- next if fcb.type == BlockType::SHELL
630
-
631
- fcb.oname.scan(regexp) do |(expression, variable)|
632
- expression.match(regexp)
633
- variables_count[$LAST_MATCH_INFO[:variable]] += 1
634
- end
635
- end
636
- # !!v variables_count
637
-
638
- # commands to echo variables
639
- #
640
- commands = {}
641
- variables_count.each do |variable, count|
642
- command = format(echo_command_form, variable)
643
- commands[variable] = command
644
- end
645
- # !!v commands
646
-
647
- # replacement dictionary from evaluated commands
648
- #
649
- replacement_dictionary = evaluate_shell_expressions(
650
- link_state.inherited_lines_block, commands,
651
- key_format: "${%s}" # no need to escape variable name for regexp
652
- ) # !!t
653
- return if replacement_dictionary.nil?
654
-
655
- # update blocks
656
- #
657
- Regexp.union(replacement_dictionary.keys).tap do |pattern|
658
- menu_blocks.each do |fcb|
659
- next if fcb.type == BlockType::SHELL
660
-
661
- fcb.variable_expansion!(pattern, replacement_dictionary)
662
- end
663
- end
664
- end
665
-
666
- private
667
-
668
- def replace_keys_in_lines(replacement_dictionary, lines)
669
- # Create a regex pattern that matches any key in the replacement dictionary
670
- pattern = Regexp.union(replacement_dictionary.keys.map { |key|
671
- "%<#{key}>"
672
- })
673
-
674
- # Iterate over each line and apply gsub with the replacement hash
675
- lines.map do |line|
676
- line.gsub(pattern) { |match| replacement_dictionary[match] }
677
- end
678
- end
679
-
680
- def add_back_option(menu_blocks:)
681
- append_chrome_block(menu_blocks: menu_blocks, menu_state: MenuState::BACK)
682
- end
683
-
684
- def add_exit_option(menu_blocks:)
685
- append_chrome_block(menu_blocks: menu_blocks, menu_state: MenuState::EXIT)
686
- end
687
-
688
- def add_inherited_lines(menu_blocks:, link_state:)
689
- append_inherited_lines(menu_blocks: menu_blocks, link_state: link_state)
639
+ append_divider(id: "#{id}.init", menu_blocks: menu_blocks,
640
+ position: :initial)
641
+ append_divider(id: "#{id}.final", menu_blocks: menu_blocks,
642
+ position: :final)
690
643
  end
691
644
 
692
645
  public
@@ -695,7 +648,7 @@ module MarkdownExec
695
648
  #
696
649
  # @param all_blocks [Array] The current blocks in the menu
697
650
  # @param type [Symbol] The type of chrome block to add (:back or :exit)
698
- def append_chrome_block(menu_blocks:, menu_state:)
651
+ def append_chrome_block(menu_blocks:, menu_state:, id: '')
699
652
  case menu_state
700
653
  when MenuState::BACK
701
654
  history_state_partition
@@ -733,6 +686,8 @@ module MarkdownExec
733
686
  dname: HashDelegator.new(@delegate_object).string_send_color(
734
687
  formatted_name, :menu_chrome_color
735
688
  ),
689
+ id: id,
690
+ type: BlockType::CHROME,
736
691
  nickname: formatted_name,
737
692
  oname: formatted_name
738
693
  )
@@ -751,10 +706,10 @@ module MarkdownExec
751
706
  #
752
707
  # @param menu_blocks [Array] The array of menu block elements.
753
708
  # @param position [Symbol] The position to insert the divider (:initial or :final).
754
- def append_divider(menu_blocks:, position:)
709
+ def append_divider(id: '', menu_blocks:, position:)
755
710
  return unless divider_formatting_present?(position)
756
711
 
757
- divider = create_divider(position)
712
+ divider = create_divider(position, id: id)
758
713
  position == :initial ? menu_blocks.unshift(divider) : menu_blocks.push(divider)
759
714
  end
760
715
 
@@ -772,7 +727,7 @@ module MarkdownExec
772
727
  { line: line })
773
728
  FCB.new(
774
729
  chrome: true,
775
- disabled: '',
730
+ disabled: TtyMenu::DISABLE,
776
731
  dname: HashDelegator.new(@delegate_object).string_send_color(
777
732
  formatted, :menu_inherited_lines_color
778
733
  ),
@@ -840,8 +795,10 @@ module MarkdownExec
840
795
  register_console_attributes(@delegate_object)
841
796
  @decor_patterns_from_delegate_object_for_block_create = collect_line_decor_patterns(@delegate_object)
842
797
 
798
+ count = 0
843
799
  blocks = []
844
800
  iter_blocks_from_nested_files do |btype, fcb|
801
+ count += 1
845
802
  case btype
846
803
  when :blocks
847
804
  if @delegate_object[:bash]
@@ -849,6 +806,7 @@ module MarkdownExec
849
806
  block_calls_scan: @delegate_object[:block_calls_scan],
850
807
  block_name_match: @delegate_object[:block_name_match],
851
808
  block_name_nick_match: @delegate_object[:block_name_nick_match],
809
+ id: "*#{count}",
852
810
  ) do |oname, color|
853
811
  apply_block_type_color_option(oname, color)
854
812
  end
@@ -858,7 +816,8 @@ module MarkdownExec
858
816
  %i[blocks line]
859
817
  when :line
860
818
  unless @delegate_object[:no_chrome]
861
- create_and_add_chrome_blocks(blocks, fcb)
819
+ create_and_add_chrome_blocks(blocks, fcb, id: "*#{count}",
820
+ init_ids: init_ids)
862
821
  end
863
822
  end
864
823
  end
@@ -894,6 +853,13 @@ module MarkdownExec
894
853
 
895
854
  # private
896
855
 
856
+ def build_replacement_dictionary(commands, link_state)
857
+ evaluate_shell_expressions(
858
+ link_state.inherited_lines_block, commands,
859
+ key_format: "${%s}" # no need to escape variable name for regexp
860
+ ) # !!t
861
+ end
862
+
897
863
  def calc_logged_stdout_filename(block_name:)
898
864
  return unless @delegate_object[:saved_stdout_folder]
899
865
 
@@ -937,13 +903,39 @@ module MarkdownExec
937
903
  true
938
904
  end
939
905
 
906
+ def chrome_block_criteria
907
+ [
908
+ { center: :table_center, format: :menu_note_format,
909
+ match: :menu_table_rows_match, type: BlockType::TEXT },
910
+ { case_conversion: :upcase, center: :heading1_center,
911
+ collapse: :heading1_collapse, collapsible: :heading1_collapsible,
912
+ color: :menu_heading1_color, format: :menu_heading1_format, level: 1,
913
+ match: :heading1_match, type: BlockType::HEADING, wrap: true },
914
+ { center: :heading2_center,
915
+ collapse: :heading2_collapse, collapsible: :heading2_collapsible,
916
+ color: :menu_heading2_color, format: :menu_heading2_format, level: 2,
917
+ match: :heading2_match, type: BlockType::HEADING, wrap: true },
918
+ { case_conversion: :downcase, center: :heading3_center,
919
+ collapse: :heading3_collapse, collapsible: :heading3_collapsible,
920
+ color: :menu_heading3_color, format: :menu_heading3_format, level: 3,
921
+ match: :heading3_match, type: BlockType::HEADING, wrap: true },
922
+ { center: :divider4_center,
923
+ collapse: :divider4_collapse, collapsible: :divider4_collapsible,
924
+ color: :menu_divider_color, format: :menu_divider_format, level: 4,
925
+ match: :divider_match, type: BlockType::DIVIDER },
926
+ { color: :menu_note_color, format: :menu_note_format,
927
+ match: :menu_note_match, type: BlockType::TEXT, wrap: true },
928
+ { color: :menu_task_color, format: :menu_task_format,
929
+ match: :menu_task_match, type: BlockType::TEXT, wrap: true }
930
+ ]
931
+ end
932
+
933
+ # sets ENV
940
934
  def code_from_vars_block_to_set_environment_variables(selected)
941
935
  code_lines = []
942
936
  YAML.load(selected.body.join("\n"))&.each do |key, value|
943
937
  ENV[key] = value.to_s
944
-
945
- require 'shellwords'
946
- code_lines.push "#{key}=\"#{Shellwords.escape(value)}\""
938
+ code_lines.push "#{key}=#{Shellwords.escape(value)}"
947
939
 
948
940
  next unless @delegate_object[:menu_vars_set_format].present?
949
941
 
@@ -971,7 +963,11 @@ module MarkdownExec
971
963
  end
972
964
  end
973
965
 
974
- def command_execute(command, shell:, args: [])
966
+ def command_execute(
967
+ command,
968
+ erls:,
969
+ shell:, args: []
970
+ )
975
971
  @run_state.files = StreamsOut.new
976
972
  @run_state.options = @delegate_object
977
973
  @run_state.started_at = Time.now.utc
@@ -983,6 +979,7 @@ module MarkdownExec
983
979
  @run_state.in_own_window = true
984
980
  command_execute_in_own_window(
985
981
  args: args,
982
+ erls: erls,
986
983
  script: @delegate_object[:execute_command_format]
987
984
  )
988
985
 
@@ -990,6 +987,7 @@ module MarkdownExec
990
987
  @run_state.in_own_window = false
991
988
  command_execute_in_process(
992
989
  args: args, command: command,
990
+ erls: erls,
993
991
  filename: @delegate_object[:filename], shell: shell
994
992
  )
995
993
  end
@@ -1007,18 +1005,27 @@ module MarkdownExec
1007
1005
  @fout.fout "Error ENOENT: #{err.inspect}"
1008
1006
  end
1009
1007
 
1010
- def command_execute_in_own_window(args:, script:)
1008
+ def command_execute_in_own_window(
1009
+ args:,
1010
+ erls:,
1011
+ script:
1012
+ )
1011
1013
  system(
1012
1014
  format(
1013
1015
  script,
1014
1016
  command_execute_in_own_window_format_arguments(
1017
+ erls: erls,
1015
1018
  rest: args ? args.join(' ') : ''
1016
1019
  )
1017
1020
  )
1018
1021
  )
1019
1022
  end
1020
1023
 
1021
- def command_execute_in_own_window_format_arguments(home: Dir.pwd, rest: '')
1024
+ def command_execute_in_own_window_format_arguments(
1025
+ home: Dir.pwd,
1026
+ erls:,
1027
+ rest: ''
1028
+ )
1022
1029
  {
1023
1030
  batch_index: @run_state.batch_index,
1024
1031
  batch_random: @run_state.batch_random,
@@ -1030,6 +1037,7 @@ module MarkdownExec
1030
1037
  @delegate_object[:logged_stdout_filespec]
1031
1038
  ),
1032
1039
  output_filespec: @delegate_object[:logged_stdout_filespec],
1040
+ play_command: erls[:play_bin],
1033
1041
  rest: rest,
1034
1042
  script_filename: @run_state.saved_filespec,
1035
1043
  script_filespec: File.join(home, @run_state.saved_filespec),
@@ -1039,7 +1047,11 @@ module MarkdownExec
1039
1047
  }
1040
1048
  end
1041
1049
 
1042
- def command_execute_in_process(args:, command:, filename:, shell:)
1050
+ def command_execute_in_process(
1051
+ args:, command:,
1052
+ erls:,
1053
+ filename:, shell:
1054
+ )
1043
1055
  execute_command_with_streams(
1044
1056
  [shell, '-c', command,
1045
1057
  @delegate_object[:filename], *args]
@@ -1061,6 +1073,19 @@ module MarkdownExec
1061
1073
  def compile_execute_and_trigger_reuse(
1062
1074
  mdoc:, selected:, block_source:, link_state:
1063
1075
  )
1076
+ # play_bin matches the name in mde.applescript, called by .mde.macos.yml
1077
+ bim = @delegate_object[:block_interactive_match]
1078
+ play_bin = if bim.present? && selected.start_line =~ Regexp.new(bim)
1079
+ @delegate_object[:play_bin_interactive]
1080
+ else
1081
+ bbm = @delegate_object[:block_batch_match]
1082
+ if bbm.present? && selected.start_line =~ Regexp.new(bbm)
1083
+ @delegate_object[:play_bin_batch]
1084
+ else
1085
+ @delegate_object[:document_play_bin]
1086
+ end
1087
+ end
1088
+
1064
1089
  required_lines = execute_block_type_port_code_lines(
1065
1090
  mdoc: mdoc, selected: selected,
1066
1091
  link_state: link_state, block_source: block_source
@@ -1080,8 +1105,10 @@ module MarkdownExec
1080
1105
  end
1081
1106
 
1082
1107
  if allow_execution
1083
- execute_required_lines(required_lines: required_lines,
1084
- selected: selected,
1108
+ execute_required_lines(blockname: selected.pub_name,
1109
+ erls: { play_bin: play_bin,
1110
+ shell: selected.shell },
1111
+ required_lines: required_lines,
1085
1112
  shell: selected.shell)
1086
1113
  end
1087
1114
 
@@ -1117,6 +1144,26 @@ module MarkdownExec
1117
1144
  HashDelegator.count_matches_in_lines(lines, regex) / 2
1118
1145
  end
1119
1146
 
1147
+ def count_named_group_occurrences(
1148
+ blocks, pattern, exclude_types: [BlockType::SHELL]
1149
+ )
1150
+ # Initialize a counter for named group occurrences
1151
+ occurrence_count = Hash.new(0)
1152
+
1153
+ blocks.each do |block|
1154
+ # Skip processing for shell-type blocks
1155
+ next if exclude_types.include?(block.type)
1156
+
1157
+ # Scan each block name for matches of the pattern
1158
+ block.oname.scan(pattern) do |(_, variable_name)|
1159
+ pattern.match($LAST_MATCH_INFO.to_s) # Reapply match for named groups
1160
+ occurrence_count[$LAST_MATCH_INFO[:variable]] += 1
1161
+ end
1162
+ end
1163
+
1164
+ occurrence_count
1165
+ end
1166
+
1120
1167
  ##
1121
1168
  # Creates and adds a formatted block to the blocks array
1122
1169
  # based on the provided match and format options.
@@ -1129,13 +1176,17 @@ module MarkdownExec
1129
1176
  # to the block's display name.
1130
1177
  # return number of lines added
1131
1178
  def create_and_add_chrome_block(blocks:, match_data:,
1179
+ collapse: nil,
1132
1180
  format_option:, color_method:,
1133
1181
  case_conversion: nil,
1134
1182
  center: nil,
1135
1183
  decor_patterns: [],
1184
+ disabled: true,
1185
+ id: '',
1186
+ level: 0,
1187
+ type: '',
1136
1188
  wrap: nil)
1137
1189
  line_cap = NamedCaptureExtractor::extract_named_group2(match_data)
1138
-
1139
1190
  # replace tabs in indent
1140
1191
  line_cap[:indent] ||= ''
1141
1192
  line_cap[:indent] = line_cap[:indent].dup if line_cap[:indent].frozen?
@@ -1145,23 +1196,19 @@ module MarkdownExec
1145
1196
  line_cap[:text] = line_cap[:text].dup if line_cap[:text].frozen?
1146
1197
  line_cap[:text].gsub!("\t", ' ')
1147
1198
  # missing capture
1199
+ line_cap[:collapse] ||= ''
1148
1200
  line_cap[:line] ||= ''
1149
1201
 
1150
1202
  accepted_width = @delegate_object[:console_width] - 2
1151
- line_caps = if wrap
1152
- if line_cap[:text].length > accepted_width
1153
- wrapper = StringWrapper.new(
1154
- width: accepted_width - line_cap[:indent].length
1155
- )
1156
- wrapper.wrap(line_cap[:text]).map do |line|
1157
- line_cap.dup.merge(text: line)
1158
- end
1159
- else
1160
- [line_cap]
1161
- end
1162
- else
1163
- [line_cap]
1164
- end
1203
+
1204
+ line_caps = [line_cap]
1205
+ if wrap && line_cap[:text].length > accepted_width
1206
+ wrapper = StringWrapper.new(width: accepted_width - line_cap[:indent].length)
1207
+ line_caps = wrapper.wrap(line_cap[:text]).map do |wrapped_line|
1208
+ line_cap.dup.merge(text: wrapped_line)
1209
+ end
1210
+ end
1211
+
1165
1212
  if center
1166
1213
  line_caps.each do |line_obj|
1167
1214
  line_obj[:indent] =
@@ -1173,7 +1220,7 @@ module MarkdownExec
1173
1220
  end
1174
1221
  end
1175
1222
 
1176
- line_caps.each do |line_obj|
1223
+ line_caps.each_with_index do |line_obj, index|
1177
1224
  next if line_obj[:text].nil?
1178
1225
 
1179
1226
  case case_conversion
@@ -1196,10 +1243,23 @@ module MarkdownExec
1196
1243
 
1197
1244
  line_obj[:line] = line_obj[:indent] + line_obj[:text]
1198
1245
  blocks.push FCB.new(
1246
+ center: center,
1199
1247
  chrome: true,
1200
- disabled: '',
1248
+ collapse: collapse.nil? ? (line_obj[:collapse] == COLLAPSIBLE_TOKEN_COLLAPSE) : collapse,
1249
+ token: line_obj[:collapse],
1250
+ disabled: disabled ? TtyMenu::DISABLE : nil,
1251
+ ####
1252
+ # id: "#{@delegate_object[:filename]}:#{index}",
1253
+ id: "#{id}.#{index}",
1254
+ level: level,
1255
+ s0indent: indent,
1256
+ s0printable: line_obj[:text],
1257
+ s1decorated: decorated,
1201
1258
  dname: line_obj[:indent] + decorated,
1202
- oname: line_obj[:text]
1259
+ indent: line_obj[:indent],
1260
+ oname: line_obj[:text],
1261
+ text: line_obj[:text],
1262
+ type: type
1203
1263
  )
1204
1264
  end
1205
1265
  line_caps.count
@@ -1213,20 +1273,8 @@ module MarkdownExec
1213
1273
  # @param opts [Hash] Options containing configuration for line processing.
1214
1274
  # @param use_chrome [Boolean] Indicates if the chrome styling should
1215
1275
  # be applied.
1216
- def create_and_add_chrome_blocks(blocks, fcb)
1217
- # rubocop:disable Layout/LineLength
1218
- match_criteria = [
1219
- { format: :menu_note_format, match: :menu_table_rows_match },
1220
- { color: :menu_heading1_color, format: :menu_heading1_format, match: :heading1_match, wrap: true, center: :heading1_center, case_conversion: :upcase },
1221
- { color: :menu_heading2_color, format: :menu_heading2_format, match: :heading2_match, wrap: true, center: :heading2_center },
1222
- { color: :menu_heading3_color, format: :menu_heading3_format, match: :heading3_match, wrap: true, center: :heading3_center, case_conversion: :downcase },
1223
- { color: :menu_divider_color, format: :menu_divider_format, match: :menu_divider_match },
1224
- { color: :menu_note_color, format: :menu_note_format, match: :menu_note_match, wrap: true },
1225
- { color: :menu_task_color, format: :menu_task_format, match: :menu_task_match, wrap: true }
1226
- ]
1227
- # rubocop:enable Layout/LineLength
1228
- # rubocop:enable Style/UnlessElse
1229
- match_criteria.each do |criteria|
1276
+ def create_and_add_chrome_blocks(blocks, fcb, id: '', init_ids: false)
1277
+ chrome_block_criteria.each_with_index do |criteria, index|
1230
1278
  unless @delegate_object[criteria[:match]].present? &&
1231
1279
  (mbody = fcb.body[0].match @delegate_object[criteria[:match]])
1232
1280
  next
@@ -1237,20 +1285,34 @@ module MarkdownExec
1237
1285
  case_conversion: criteria[:case_conversion],
1238
1286
  center: criteria[:center] &&
1239
1287
  @delegate_object[criteria[:center]],
1288
+
1289
+ collapse: case fcb.collapse_token
1290
+ when COLLAPSIBLE_TOKEN_COLLAPSE
1291
+ true
1292
+ when COLLAPSIBLE_TOKEN_EXPAND
1293
+ false
1294
+ else
1295
+ false####
1296
+ end,
1297
+
1240
1298
  color_method: criteria[:color] &&
1241
1299
  @delegate_object[criteria[:color]].to_sym,
1242
1300
  decor_patterns:
1243
1301
  @decor_patterns_from_delegate_object_for_block_create,
1302
+ disabled: !(criteria[:collapsible] && @delegate_object[criteria[:collapsible]]),
1303
+ id: "#{id}.#{index}",
1244
1304
  format_option: criteria[:format] &&
1245
1305
  @delegate_object[criteria[:format]],
1306
+ level: criteria[:level],
1246
1307
  match_data: mbody,
1308
+ type: criteria[:type],
1247
1309
  wrap: criteria[:wrap]
1248
1310
  )
1249
1311
  break
1250
1312
  end
1251
1313
  end
1252
1314
 
1253
- def create_divider(position)
1315
+ def create_divider(position, id: '')
1254
1316
  divider_key = if position == :initial
1255
1317
  :menu_initial_divider
1256
1318
  else
@@ -1261,8 +1323,9 @@ module MarkdownExec
1261
1323
 
1262
1324
  FCB.new(
1263
1325
  chrome: true,
1264
- disabled: '',
1326
+ disabled: TtyMenu::DISABLE,
1265
1327
  dname: string_send_color(oname, :menu_divider_color),
1328
+ id: id,
1266
1329
  oname: oname
1267
1330
  )
1268
1331
  end
@@ -1344,7 +1407,9 @@ module MarkdownExec
1344
1407
  end
1345
1408
 
1346
1409
  def dml_menu_append_chrome_item(
1347
- name, count, type, menu_state: MenuState::LOAD,
1410
+ name, count, type,
1411
+ id: '',
1412
+ menu_state: MenuState::LOAD,
1348
1413
  always_create: true, always_enable: true
1349
1414
  )
1350
1415
  raise unless name.present?
@@ -1355,7 +1420,8 @@ module MarkdownExec
1355
1420
  # create menu item when it is needed (count > 0)
1356
1421
  #
1357
1422
  if item.nil? && (always_create || count.positive?)
1358
- item = append_chrome_block(menu_blocks: @dml_menu_blocks,
1423
+ item = append_chrome_block(id: id,
1424
+ menu_blocks: @dml_menu_blocks,
1359
1425
  menu_state: menu_state)
1360
1426
  end
1361
1427
 
@@ -1367,7 +1433,7 @@ module MarkdownExec
1367
1433
  if always_enable || count.positive?
1368
1434
  item.delete(:disabled)
1369
1435
  else
1370
- item[:disabled] = ''
1436
+ item[:disabled] = TtyMenu::DISABLE
1371
1437
  end
1372
1438
  end
1373
1439
 
@@ -1521,18 +1587,15 @@ module MarkdownExec
1521
1587
  def execute_block_by_type_for_lfls(
1522
1588
  selected:, mdoc:, block_source:, link_state: LinkState.new
1523
1589
  )
1524
- # !!v selected
1525
1590
  # order should not be important other than else clause
1526
1591
  if selected.type == BlockType::EDIT
1527
1592
  debounce_reset
1528
- # !!v link_state.inherited_lines_block
1529
1593
  vux_edit_inherited
1530
1594
  return :break if pause_user_exit
1531
1595
 
1532
1596
  next_state_append_code(selected, link_state, [])
1533
1597
 
1534
1598
  elsif selected.type == BlockType::HISTORY
1535
- # !!b
1536
1599
  debounce_reset
1537
1600
  return :break if execute_block_type_history_ux(
1538
1601
  selected: selected,
@@ -1544,10 +1607,10 @@ module MarkdownExec
1544
1607
  elsif selected.type == BlockType::LINK
1545
1608
  debounce_reset
1546
1609
  execute_block_type_link_with_state(link_block_body: selected.body,
1547
- mdoc: mdoc,
1548
- selected: selected,
1549
- link_state: link_state,
1550
- block_source: block_source)
1610
+ mdoc: mdoc,
1611
+ selected: selected,
1612
+ link_state: link_state,
1613
+ block_source: block_source)
1551
1614
 
1552
1615
  elsif selected.type == BlockType::LOAD
1553
1616
  debounce_reset
@@ -1611,6 +1674,15 @@ module MarkdownExec
1611
1674
  next_state_append_code(selected, link_state,
1612
1675
  code_from_vars_block_to_set_environment_variables(selected))
1613
1676
 
1677
+ elsif COLLAPSIBLE_TYPES.include?(selected.type)
1678
+ debounce_reset
1679
+ if @compressed_ids.keys.include?(selected.id)
1680
+ @compressed_ids.delete(selected.id)
1681
+ else
1682
+ @compressed_ids.merge!(selected.id => selected.level)
1683
+ end
1684
+ LoadFileLinkState.new(LoadFile::REUSE, link_state)
1685
+
1614
1686
  elsif debounce_allows
1615
1687
  compile_execute_and_trigger_reuse(mdoc: mdoc,
1616
1688
  selected: selected,
@@ -1695,9 +1767,7 @@ module MarkdownExec
1695
1767
  link_block_body: [], mdoc: nil, selected: FCB.new,
1696
1768
  link_state: LinkState.new, block_source: {}
1697
1769
  )
1698
- # !!p link_block_body selected
1699
1770
  link_block_data = HashDelegator.parse_yaml_data_from_body(link_block_body)
1700
- # !!v link_block_data
1701
1771
  ## collect blocks specified by block
1702
1772
  #
1703
1773
  if mdoc
@@ -1791,16 +1861,15 @@ module MarkdownExec
1791
1861
  def execute_block_type_load_code_lines(
1792
1862
  selected,
1793
1863
  directory: @delegate_object[:document_configurations_directory],
1864
+ exit_prompt: @delegate_object[:prompt_filespec_back],
1794
1865
  filename_pattern: @delegate_object[:vars_block_filename_pattern],
1795
1866
  glob: @delegate_object[:document_configurations_glob],
1796
1867
  view: @delegate_object[:vars_block_filename_view]
1797
1868
  )
1798
- # !!p selected
1799
1869
  block_data = HashDelegator.parse_yaml_data_from_body(selected.body)
1800
- # !!v block_data
1801
1870
  if selected_option = select_option_with_metadata(
1802
1871
  prompt_title,
1803
- Dir.glob(
1872
+ [exit_prompt] + Dir.glob(
1804
1873
  File.join(
1805
1874
  Dir.pwd,
1806
1875
  block_data['directory'] || directory,
@@ -1808,19 +1877,21 @@ module MarkdownExec
1808
1877
  )
1809
1878
  ).sort.map do |file|
1810
1879
  { name: format(
1811
- block_data['view'] || view,
1812
- NamedCaptureExtractor::extract_named_group2(
1813
- file.match(
1814
- Regexp.new(block_data['filename_pattern'] ||
1815
- filename_pattern)
1816
- )
1880
+ block_data['view'] || view,
1881
+ NamedCaptureExtractor::extract_named_group2(
1882
+ file.match(
1883
+ Regexp.new(block_data['filename_pattern'] ||
1884
+ filename_pattern)
1817
1885
  )
1818
- ),
1886
+ )
1887
+ ),
1819
1888
  oname: file }
1820
1889
  end,
1821
1890
  simple_menu_options
1822
1891
  )
1823
- File.readlines(selected_option.oname, chomp: true)
1892
+ if selected_option.dname != exit_prompt
1893
+ File.readlines(selected_option.oname, chomp: true)
1894
+ end
1824
1895
  else
1825
1896
  warn "No matching files found" ###
1826
1897
  end
@@ -1835,7 +1906,7 @@ module MarkdownExec
1835
1906
  # @param selected [Hash] The selected block.
1836
1907
  # @return [Array<String>] Required code blocks as an array of lines.
1837
1908
  def execute_block_type_port_code_lines(mdoc:, selected:, block_source:,
1838
- link_state: LinkState.new)
1909
+ link_state: LinkState.new)
1839
1910
  required = mdoc.collect_recursively_required_code(
1840
1911
  anyname: selected.pub_name,
1841
1912
  label_format_above: @delegate_object[:shell_code_label_format_above],
@@ -1881,24 +1952,19 @@ module MarkdownExec
1881
1952
  end
1882
1953
 
1883
1954
  def execute_block_type_save(code_lines:, selected:)
1884
- # !!p code_lines, selected
1885
1955
  block_data = HashDelegator.parse_yaml_data_from_body(selected.body)
1886
- # !!v block_data
1887
1956
  directory_glob = if block_data['directory']
1888
- # !!b
1889
1957
  File.join(
1890
1958
  block_data['directory'],
1891
1959
  block_data['glob'] ||
1892
1960
  @delegate_object[:document_saved_lines_glob].split('/').last
1893
1961
  )
1894
1962
  else
1895
- # !!b
1896
1963
  @delegate_object[:document_saved_lines_glob]
1897
1964
  end
1898
- # !!v directory_glob
1899
1965
 
1900
1966
  save_filespec_from_expression(directory_glob).tap do |save_filespec|
1901
- if save_filespec
1967
+ if save_filespec && save != exit_prompt
1902
1968
  begin
1903
1969
  File.write(save_filespec,
1904
1970
  HashDelegator.join_code_lines(code_lines))
@@ -2049,20 +2115,63 @@ module MarkdownExec
2049
2115
  # @param required_lines [Array<String>] The lines of code to be executed.
2050
2116
  # @param selected [FCB] The selected functional code block object.
2051
2117
  def execute_required_lines(
2052
- required_lines: [], selected: FCB.new, shell:
2118
+ blockname: '',
2119
+ erls: {},
2120
+ required_lines: [], shell:
2053
2121
  )
2054
2122
  if @delegate_object[:save_executed_script]
2055
- write_command_file(required_lines: required_lines,
2056
- selected: selected,
2123
+ write_command_file(blockname: blockname,
2124
+ required_lines: required_lines,
2057
2125
  shell: shell)
2058
2126
  end
2059
2127
  if @dml_block_state
2060
2128
  calc_logged_stdout_filename(block_name: @dml_block_state.block.oname)
2061
2129
  end
2062
- format_and_execute_command(code_lines: required_lines, shell: shell)
2130
+ format_and_execute_command(
2131
+ code_lines: required_lines,
2132
+ erls: erls,
2133
+ shell: shell
2134
+ )
2063
2135
  post_execution_process
2064
2136
  end
2065
2137
 
2138
+ def expand_blocks_with_replacements(
2139
+ menu_blocks, replacements, exclude_types: [BlockType::SHELL]
2140
+ )
2141
+ # update blocks
2142
+ #
2143
+ Regexp.union(replacements.keys).tap do |pattern|
2144
+ menu_blocks.each do |block|
2145
+ next if exclude_types.include?(block.type)
2146
+
2147
+ block.expand_variables_in_attributes!(pattern, replacements)
2148
+ end
2149
+ end
2150
+ end
2151
+
2152
+ def expand_variable_references!(
2153
+ # echo_format: 'echo "$%s"',
2154
+ echo_format: 'echo $%s',
2155
+ link_state:,
2156
+ blocks:,
2157
+ pattern: Regexp.new(@delegate_object[:variable_expression_regexp])
2158
+ )
2159
+ # Count occurrences of named groups in each block
2160
+ variable_counts = count_named_group_occurrences(blocks, pattern)
2161
+
2162
+ # Generate echo commands for each variable based on its count
2163
+ echo_commands = generate_echo_commands(variable_counts, echo_format)
2164
+
2165
+ # Build a dictionary to replace variables with the corresponding commands
2166
+ replacements = build_replacement_dictionary(echo_commands, link_state)
2167
+
2168
+ # Exit early if no replacements are needed
2169
+ return if replacements.nil?
2170
+
2171
+ # Expand each block with replacements from the dictionary
2172
+ expand_blocks_with_replacements(blocks, replacements)
2173
+ end
2174
+
2066
2175
  # Retrieves a specific data symbol from the delegate object,
2067
2176
  # converts it to a string, and applies a color style
2068
2177
  # based on the specified color symbol.
@@ -2096,11 +2205,20 @@ module MarkdownExec
2096
2205
  { size: file_size, lines: line_count }
2097
2206
  end
2098
2207
 
2099
- def format_and_execute_command(code_lines:, shell:)
2208
+ def format_and_execute_command(
2209
+ code_lines:,
2210
+ erls:,
2211
+ shell:
2212
+ )
2100
2213
  formatted_command = code_lines.flatten.join("\n")
2101
2214
  @fout.fout fetch_color(data_sym: :script_execution_head,
2102
2215
  color_sym: :script_execution_frame_color)
2103
- command_execute(formatted_command, args: @pass_args, shell: shell)
2216
+ command_execute(
2217
+ formatted_command,
2218
+ args: @pass_args,
2219
+ erls: erls,
2220
+ shell: shell
2221
+ )
2104
2222
  @fout.fout fetch_color(data_sym: :script_execution_tail,
2105
2223
  color_sym: :script_execution_frame_color)
2106
2224
  end
@@ -2162,6 +2280,17 @@ module MarkdownExec
2162
2280
  color_sym: :execution_report_preview_frame_color)
2163
2281
  end
2164
2282
 
2283
+ def generate_echo_commands(variable_counts, echo_format)
2284
+ # commands to echo variables
2285
+ #
2286
+ commands = {}
2287
+ variable_counts.each do |variable, count|
2288
+ command = format(echo_format, variable)
2289
+ commands[variable] = command
2290
+ end
2291
+ commands
2292
+ end
2293
+
2165
2294
  def generate_temp_filename(ext = '.sh')
2166
2295
  filename = begin
2167
2296
  Dir::Tmpname.make_tmpname(['x', ext], nil)
@@ -2219,12 +2348,9 @@ module MarkdownExec
2219
2348
  order: :chronological,
2220
2349
  path: ''
2221
2350
  )
2222
- # !!v filename, 'path', path
2223
- # !!v File.join(home, path, filename)
2224
2351
  files = Dir.glob(
2225
2352
  File.join(home, path, filename)
2226
2353
  )
2227
- # !!v files
2228
2354
  sorted_files = case order
2229
2355
  when :alphabetical
2230
2356
  files.sort
@@ -2267,16 +2393,18 @@ module MarkdownExec
2267
2393
  cfile.readlines(
2268
2394
  @delegate_object[:filename],
2269
2395
  import_paths: @delegate_object[:import_paths]&.split(':')
2270
- ).each do |nested_line|
2396
+ ).each_with_index do |nested_line, index|
2271
2397
  if nested_line
2272
- update_line_and_block_state(nested_line, state, selected_types,
2273
- &block)
2398
+ update_line_and_block_state(
2399
+ nested_line, state, selected_types,
2400
+ id: "#{@delegate_object[:filename]}:#{index}",
2401
+ &block
2402
+ )
2274
2403
  end
2275
2404
  end
2276
2405
  end
2277
2406
 
2278
2407
  def iter_source_blocks(source, &block)
2279
- # !!v source
2280
2408
  case source
2281
2409
  when 1
2282
2410
  blocks_from_nested_files.each(&block)
@@ -2400,18 +2528,14 @@ module MarkdownExec
2400
2528
  end
2401
2529
 
2402
2530
  def list_blocks
2403
- # !!b
2404
2531
  message = @delegate_object[:list_blocks_message]
2405
2532
  block_eval = @delegate_object[:list_blocks_eval]
2406
- # !!v message block_eval
2407
2533
 
2408
2534
  list = []
2409
2535
  iter_source_blocks(@delegate_object[:list_blocks_type]) do |block|
2410
- # !!v block
2411
2536
  list << (block_eval.present? ? eval(block_eval) : block.send(message))
2412
2537
  end
2413
2538
  list.compact!
2414
- # !!v list
2415
2539
 
2416
2540
  @fout.fout_list(list)
2417
2541
  end
@@ -2421,10 +2545,10 @@ module MarkdownExec
2421
2545
  # Executes a specified block once per filename.
2422
2546
  # @param all_blocks [Array] Array of all block elements.
2423
2547
  # @return [Boolean, nil] True if values were modified, nil otherwise.
2424
- def load_auto_opts_block(all_blocks, mdoc:)
2548
+ def load_auto_opts_block(all_blocks, id: '', mdoc:)
2425
2549
  block_name = @delegate_object[:document_load_opts_block_name]
2426
2550
  unless block_name.present? &&
2427
- @most_recent_loaded_filename != @delegate_object[:filename]
2551
+ @opts_most_recent_filename != @delegate_object[:filename]
2428
2552
  return
2429
2553
  end
2430
2554
 
@@ -2437,13 +2561,26 @@ module MarkdownExec
2437
2561
  )
2438
2562
  update_menu_base(options_state.options)
2439
2563
 
2440
- @most_recent_loaded_filename = @delegate_object[:filename]
2564
+ @opts_most_recent_filename = @delegate_object[:filename]
2441
2565
  true
2442
2566
  end
2443
2567
 
2568
+ def load_auto_vars_block(all_blocks,
2569
+ block_name: @delegate_object[:document_load_vars_block_name])
2570
+ unless block_name.present? &&
2571
+ @vars_most_recent_filename != @delegate_object[:filename]
2572
+ return
2573
+ end
2574
+
2575
+ block = HashDelegator.block_find(all_blocks, :oname, block_name)
2576
+ return unless block
2577
+
2578
+ @vars_most_recent_filename = @delegate_object[:filename]
2579
+ code_from_vars_block_to_set_environment_variables(block)
2580
+ end
2581
+
2444
2582
  def load_cli_or_user_selected_block(all_blocks: [], menu_blocks: [],
2445
2583
  default: nil)
2446
- # !!b
2447
2584
  if @delegate_object[:block_name].present?
2448
2585
  block = all_blocks.find do |item|
2449
2586
  item.pub_name == @delegate_object[:block_name]
@@ -2505,7 +2642,6 @@ module MarkdownExec
2505
2642
  link_state:)
2506
2643
  if block_name_from_cli &&
2507
2644
  @cli_block_name == @menu_base_options[:menu_persist_block_name]
2508
- # !!b 'pause cli control, allow user to select block'
2509
2645
  block_name_from_cli = false
2510
2646
  now_using_cli = false
2511
2647
  @menu_base_options[:block_name] =
@@ -2530,24 +2666,48 @@ module MarkdownExec
2530
2666
  ## Handles the file loading and returns the blocks
2531
2667
  # in the file and MDoc instance
2532
2668
  #
2533
- def mdoc_menu_and_blocks_from_nested_files(link_state)
2669
+ def mdoc_menu_and_blocks_from_nested_files(link_state, id: '')
2670
+ # read blocks, load document opts block, and re-process blocks
2671
+ #
2534
2672
  all_blocks, mdoc = mdoc_and_blocks_from_nested_files
2673
+ if load_auto_opts_block(all_blocks, id: id, mdoc: mdoc)
2674
+ all_blocks, mdoc = mdoc_and_blocks_from_nested_files
2675
+ end
2535
2676
 
2536
- # recreate menu with new options
2677
+ # load document vars block
2537
2678
  #
2538
- if load_auto_opts_block(all_blocks, mdoc: mdoc)
2539
- all_blocks, mdoc = mdoc_and_blocks_from_nested_files
2679
+ if code_lines = load_auto_vars_block(all_blocks)
2680
+ new_code = HashDelegator.code_merge(link_state.inherited_lines,
2681
+ code_lines)
2682
+ next_state_set_code(
2683
+ nil,
2684
+ link_state,
2685
+ new_code
2686
+ )
2687
+ link_state.inherited_lines = new_code
2540
2688
  end
2541
2689
 
2542
- menu_blocks = mdoc.fcbs_per_options(@delegate_object)
2690
+ # filter by name, collapsed
2691
+ #
2692
+ menu_blocks, @compressed_ids = mdoc.fcbs_per_options(
2693
+ @delegate_object.merge!(compressed_ids: @compressed_ids)
2694
+ )
2695
+
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)
2543
2700
 
2544
- variable_expansions!(menu_blocks: menu_blocks, link_state: link_state)
2545
- add_menu_chrome_blocks!(menu_blocks: menu_blocks, link_state: link_state)
2701
+ # chrome for menu
2702
+ #
2703
+ add_menu_chrome_blocks!(id: id, menu_blocks: menu_blocks,
2704
+ link_state: link_state)
2546
2705
 
2547
2706
  ### compress empty lines
2548
2707
  HashDelegator.delete_consecutive_blank_lines!(menu_blocks)
2549
- HashDelegator.tables_into_columns!(menu_blocks, @delegate_object)
2550
- [all_blocks, menu_blocks, mdoc] # !!r
2708
+ HashDelegator.tables_into_columns!(menu_blocks, @delegate_object) ####
2709
+
2710
+ [all_blocks, menu_blocks, mdoc]
2551
2711
  end
2552
2712
 
2553
2713
  def menu_add_disabled_option(document_glob)
@@ -2562,7 +2722,7 @@ module MarkdownExec
2562
2722
 
2563
2723
  chrome_block = FCB.new(
2564
2724
  chrome: true,
2565
- disabled: '',
2725
+ disabled: TtyMenu::DISABLE,
2566
2726
  dname: HashDelegator.new(@delegate_object).string_send_color(
2567
2727
  document_glob, :menu_inherited_lines_color
2568
2728
  ),
@@ -2630,7 +2790,7 @@ module MarkdownExec
2630
2790
  block_names = []
2631
2791
  dependencies = {}
2632
2792
  link_history_push_and_next(
2633
- curr_block_name: selected.pub_name,
2793
+ curr_block_name: selected&.pub_name,
2634
2794
  curr_document_filename: @delegate_object[:filename],
2635
2795
  inherited_block_names:
2636
2796
  ((link_state&.inherited_block_names || []) + block_names).sort.uniq,
@@ -2747,32 +2907,31 @@ module MarkdownExec
2747
2907
  fout_execution_report if @delegate_object[:output_execution_report]
2748
2908
  end
2749
2909
 
2750
- # Prepare the blocks menu by adding labels and other necessary details.
2751
- # Remove filtered blocks.
2910
+ # Filter blocks per block_name_include_match, block_name_wrapper_match.
2752
2911
  #
2753
2912
  # @param all_blocks [Array<Hash>] The list of blocks from the file.
2754
- # @param opts [Hash] The options hash.
2755
- # @return [Array<Hash>] The updated blocks menu.
2756
- def prepare_blocks_menu(menu_blocks)
2757
- menu_blocks.map do |fcb|
2758
- next if Filter.prepared_not_in_menu?(
2913
+ def select_blocks(menu_blocks)
2914
+ menu_blocks.select do |fcb|
2915
+ !Filter.prepared_not_in_menu?(
2759
2916
  @delegate_object,
2760
2917
  fcb,
2761
2918
  %i[block_name_include_match block_name_wrapper_match]
2762
2919
  )
2920
+ end
2921
+ end
2763
2922
 
2764
- fcb.name = fcb.dname
2765
- # fcb.label = BlockLabel.make(
2766
- # body: fcb.body,
2767
- # filename: @delegate_object[:filename],
2768
- # headings: fcb.headings,
2769
- # menu_blocks_with_docname: @delegate_object[:menu_blocks_with_docname],
2770
- # menu_blocks_with_headings: @delegate_object[:menu_blocks_with_headings],
2771
- # text: fcb.text,
2772
- # title: fcb.title
2773
- # )
2923
+ # Filter blocks per block_name_include_match, block_name_wrapper_match.
2924
+ # Set name displayed by tty-prompt.
2925
+ #
2926
+ # @param all_blocks [Array<Hash>] The list of blocks from the file.
2927
+ # @param opts [Hash] The options hash.
2928
+ # @return [Array<Hash>] The updated blocks menu.
2929
+ def blocks_as_menu_items(menu_blocks)
2930
+ 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
2774
2933
  fcb.to_h
2775
- end.compact
2934
+ end
2776
2935
  end
2777
2936
 
2778
2937
  def print_formatted_option(key, value)
@@ -3142,6 +3301,20 @@ module MarkdownExec
3142
3301
  { abort: true })
3143
3302
  end
3144
3303
 
3304
+ # private
3305
+
3306
+ def replace_keys_in_lines(replacement_dictionary, lines)
3307
+ # Create a regex pattern that matches any key in the replacement dictionary
3308
+ pattern = Regexp.union(replacement_dictionary.keys.map { |key|
3309
+ "%<#{key}>"
3310
+ })
3311
+
3312
+ # Iterate over each line and apply gsub with the replacement hash
3313
+ lines.map do |line|
3314
+ line.gsub(pattern) { |match| replacement_dictionary[match] }
3315
+ end
3316
+ end
3317
+
3145
3318
  def report_error(err)
3146
3319
  # Handle ENOENT error
3147
3320
  @run_state.aborted_at = Time.now.utc
@@ -3235,9 +3408,12 @@ module MarkdownExec
3235
3408
  end
3236
3409
  end
3237
3410
 
3238
- def save_to_file(required_lines:, selected:, shell:)
3411
+ def save_to_file(
3412
+ erls:,
3413
+ required_lines:, selected:, shell:
3414
+ )
3239
3415
  write_command_file(
3240
- required_lines: required_lines, selected: selected, shell: shell
3416
+ required_lines: required_lines, blockname: selected.pub_name, shell: shell
3241
3417
  )
3242
3418
  @fout.fout "File saved: #{@run_state.saved_filespec}"
3243
3419
  end
@@ -3254,7 +3430,6 @@ module MarkdownExec
3254
3430
  end
3255
3431
 
3256
3432
  def select_document_if_multiple(options, files, prompt:)
3257
- # binding.irb
3258
3433
  return files if files.class == String ###
3259
3434
  return files[0] if (count = files.count) == 1
3260
3435
 
@@ -3274,17 +3449,34 @@ module MarkdownExec
3274
3449
  # Presents a TTY prompt to select an option or exit,
3275
3450
  # returns metadata including option and selected
3276
3451
  def select_option_with_metadata(prompt_text, menu_items, opts = {})
3277
- # !!v prompt_text menu_items
3278
3452
  ## configure to environment
3279
3453
  #
3280
3454
  register_console_attributes(opts)
3281
3455
 
3282
- # crashes if all menu options are disabled
3456
+ active_color_pastel = Pastel.new
3457
+ active_color_pastel = opts[:menu_active_color_pastel_messages]
3458
+ .inject(active_color_pastel) do |p, message|
3459
+ p.send(message)
3460
+ end
3461
+
3283
3462
  begin
3463
+ props = {
3464
+ active_color: active_color_pastel.detach,
3465
+ # activate dynamic list searching on letter/number key presses
3466
+ filter: true,
3467
+ }.freeze
3468
+
3469
+ # crashes if all menu options are disabled
3470
+ # crashes if default is not an existing item
3471
+ #
3284
3472
  selection = @prompt.select(prompt_text,
3285
3473
  menu_items,
3286
- opts.merge(filter: true))
3287
- # !!v selection
3474
+ opts.merge(props))
3475
+ rescue TTY::Prompt::ConfigurationError
3476
+ # prompt fails when collapsible block name has changed; clear default
3477
+ selection = @prompt.select(prompt_text,
3478
+ menu_items,
3479
+ opts.merge(props).merge(default: nil))
3288
3480
  rescue NoMethodError
3289
3481
  # no enabled options in page
3290
3482
  return
@@ -3292,9 +3484,10 @@ module MarkdownExec
3292
3484
 
3293
3485
  selected = menu_items.find do |item|
3294
3486
  if item.instance_of?(Hash)
3295
- (item[:name] || item[:dname]) == selection
3487
+ # (item[:id] || item[:name] || item[:dname]) == selection
3488
+ [item[:id], item[:name], item[:dname]].include?(selection)
3296
3489
  elsif item.instance_of?(MarkdownExec::FCB)
3297
- item.dname == selection
3490
+ item.dname == selection || item.id == selection
3298
3491
  else
3299
3492
  item == selection
3300
3493
  end
@@ -3353,7 +3546,7 @@ module MarkdownExec
3353
3546
 
3354
3547
  out = `#{cmd}`.sub(/.*?#{marker}/m, '')
3355
3548
  File.delete filespec
3356
- out # !!r
3549
+ out
3357
3550
  end
3358
3551
 
3359
3552
  def should_add_back_option?(
@@ -3487,8 +3680,11 @@ module MarkdownExec
3487
3680
  # @return [Void] The function modifies the `state`
3488
3681
  # and `selected_types` arguments in place.
3489
3682
  ##
3490
- def update_line_and_block_state(nested_line, state, selected_types,
3491
- &block)
3683
+ def update_line_and_block_state(
3684
+ nested_line, state, selected_types,
3685
+ id:,
3686
+ &block
3687
+ )
3492
3688
  line = nested_line.to_s
3493
3689
  if line.match(@delegate_object[:fenced_start_and_end_regex])
3494
3690
  if state[:in_fenced_block]
@@ -3523,11 +3719,8 @@ module MarkdownExec
3523
3719
  @delegate_object[:menu_include_imported_notes]
3524
3720
  # add line if it is depth 0 or option allows it
3525
3721
  #
3526
- HashDelegator.yield_line_if_selected(line, selected_types, &block)
3527
-
3528
- else
3529
- # !!b 'line is not recognized for block state'
3530
-
3722
+ HashDelegator.yield_line_if_selected(line, selected_types, id: id,
3723
+ &block)
3531
3724
  end
3532
3725
  end
3533
3726
 
@@ -3545,13 +3738,11 @@ module MarkdownExec
3545
3738
  menu_blocks: @dml_menu_blocks,
3546
3739
  default: @dml_menu_default_dname
3547
3740
  )
3548
- # !!b '@run_state.source.block_name_from_cli:',@run_state.source.block_name_from_cli
3549
3741
  if !@dml_block_state
3550
3742
  # HashDelegator.error_handler('block_state missing', { abort: true })
3551
3743
  # document has no enabled items
3552
3744
  :break
3553
3745
  elsif @dml_block_state.state == MenuState::EXIT
3554
- # !!b 'load_cli_or_user_selected_block -> break'
3555
3746
  :break
3556
3747
  end
3557
3748
  end
@@ -3604,7 +3795,6 @@ module MarkdownExec
3604
3795
  was_using_cli: @dml_now_using_cli
3605
3796
  )
3606
3797
 
3607
- # !!b '!block_name_from_ui + cli_break -> break'
3608
3798
  !@dml_block_state.source.block_name_from_ui && cli_break && :break
3609
3799
  end
3610
3800
 
@@ -3810,23 +4000,21 @@ module MarkdownExec
3810
4000
  @delegate_object[:filename],
3811
4001
  block_list
3812
4002
  ).run do |msg, data|
3813
- # !!v msg data
3814
- # !!t msg
3815
4003
  case msg
3816
4004
  when :parse_document # once for each menu
3817
- vux_parse_document
3818
- vux_menu_append_history_files(formatted_choice_ostructs)
4005
+ vux_parse_document(id: 'vux_parse_document')
4006
+ vux_menu_append_history_files(formatted_choice_ostructs,
4007
+ id: "vux_menu_append_history_files",)
3819
4008
  vux_publish_document_file_name_for_external_automation
3820
4009
 
3821
4010
  when :display_menu
4011
+ # does not display
3822
4012
  vux_clear_menu_state
3823
4013
 
3824
4014
  when :end_of_cli
3825
- # !!b
3826
4015
  # yield :end_of_cli, @delegate_object
3827
4016
 
3828
4017
  if @delegate_object[:list_blocks]
3829
- # !!b
3830
4018
  list_blocks
3831
4019
  :exit
3832
4020
  end
@@ -3862,7 +4050,8 @@ module MarkdownExec
3862
4050
  end
3863
4051
  end
3864
4052
 
3865
- def vux_menu_append_history_files(formatted_choice_ostructs)
4053
+ def vux_menu_append_history_files(formatted_choice_ostructs,
4054
+ id: '')
3866
4055
  if @delegate_object[:menu_for_history]
3867
4056
  history_files(
3868
4057
  @dml_link_state,
@@ -3873,7 +4062,9 @@ module MarkdownExec
3873
4062
  if files.count.positive?
3874
4063
  dml_menu_append_chrome_item(
3875
4064
  formatted_choice_ostructs[:history].oname, files.count,
3876
- 'files', menu_state: MenuState::HISTORY
4065
+ 'files',
4066
+ id: id,
4067
+ menu_state: MenuState::HISTORY
3877
4068
  )
3878
4069
  end
3879
4070
  end
@@ -3895,6 +4086,7 @@ module MarkdownExec
3895
4086
  if files.count.positive?
3896
4087
  dml_menu_append_chrome_item(
3897
4088
  formatted_choice_ostructs[:load].dname, files.count, 'files',
4089
+ id: "#{id}.load",
3898
4090
  menu_state: MenuState::LOAD
3899
4091
  )
3900
4092
  end
@@ -3902,18 +4094,21 @@ module MarkdownExec
3902
4094
  lines_count.positive?
3903
4095
  dml_menu_append_chrome_item(
3904
4096
  formatted_choice_ostructs[:edit].dname, lines_count, 'lines',
4097
+ id: "#{id}.edit",
3905
4098
  menu_state: MenuState::EDIT
3906
4099
  )
3907
4100
  end
3908
4101
  if lines_count.positive?
3909
4102
  dml_menu_append_chrome_item(
3910
4103
  formatted_choice_ostructs[:save].dname, 1, '',
4104
+ id: "#{id}.save",
3911
4105
  menu_state: MenuState::SAVE
3912
4106
  )
3913
4107
  end
3914
4108
  if lines_count.positive?
3915
4109
  dml_menu_append_chrome_item(
3916
4110
  formatted_choice_ostructs[:view].dname, 1, '',
4111
+ id: "#{id}.view",
3917
4112
  menu_state: MenuState::VIEW
3918
4113
  )
3919
4114
  end
@@ -3921,6 +4116,7 @@ module MarkdownExec
3921
4116
  if @delegate_object[:menu_with_shell]
3922
4117
  dml_menu_append_chrome_item(
3923
4118
  formatted_choice_ostructs[:shell].dname, 1, '',
4119
+ id: "#{id}.shell",
3924
4120
  menu_state: MenuState::SHELL
3925
4121
  )
3926
4122
  end
@@ -3939,7 +4135,7 @@ module MarkdownExec
3939
4135
  )
3940
4136
  end
3941
4137
 
3942
- def vux_parse_document
4138
+ def vux_parse_document(id: '')
3943
4139
  @run_state.batch_index += 1
3944
4140
  @run_state.in_own_window = false
3945
4141
 
@@ -3960,10 +4156,10 @@ module MarkdownExec
3960
4156
 
3961
4157
  # update @delegate_object and @menu_base_options in auto_load
3962
4158
  #
4159
+ # @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc, @dml_link_state =
3963
4160
  @dml_blocks_in_file, @dml_menu_blocks, @dml_mdoc =
3964
- mdoc_menu_and_blocks_from_nested_files(@dml_link_state)
4161
+ mdoc_menu_and_blocks_from_nested_files(@dml_link_state, id: id)
3965
4162
  dump_delobj(@dml_blocks_in_file, @dml_menu_blocks, @dml_link_state)
3966
- # !!b 'loop', @run_state.source.block_name_from_cli, @cli_block_name
3967
4163
  end
3968
4164
 
3969
4165
  def vux_publish_block_name_for_external_automation(block_name)
@@ -3995,7 +4191,6 @@ module MarkdownExec
3995
4191
 
3996
4192
  # return :break to break from loop
3997
4193
  def vux_user_selected_block_name
3998
- # !!b
3999
4194
  if @dml_link_state.block_name.present?
4000
4195
  # @prior_block_was_link = true
4001
4196
  @dml_block_state.block = blocks_find_by_block_name(
@@ -4026,32 +4221,27 @@ module MarkdownExec
4026
4221
  end
4027
4222
 
4028
4223
  def wait_for_user_selected_block(all_blocks, menu_blocks, default)
4029
- # !!b
4030
4224
  block_state = wait_for_user_selection(all_blocks, menu_blocks, default)
4031
4225
  handle_back_or_continue(block_state)
4032
4226
  block_state
4033
4227
  end
4034
4228
 
4035
4229
  def wait_for_user_selection(_all_blocks, menu_blocks, default)
4036
- # !!b
4037
4230
  if @delegate_object[:clear_screen_for_select_block]
4038
4231
  printf("\e[1;1H\e[2J")
4039
4232
  end
4040
4233
 
4041
- # !!b
4042
4234
  prompt_title = string_send_color(
4043
4235
  @delegate_object[:prompt_select_block].to_s,
4044
4236
  :prompt_color_after_script_execution
4045
4237
  )
4046
4238
 
4047
- # !!b
4048
- menu_items = prepare_blocks_menu(menu_blocks)
4239
+ menu_items = blocks_as_menu_items(menu_blocks)
4049
4240
  if menu_items.empty?
4050
4241
  return SelectedBlockMenuState.new(nil, OpenStruct.new,
4051
4242
  MenuState::EXIT)
4052
4243
  end
4053
4244
 
4054
- # !!b
4055
4245
  # default value may not match if color is different from
4056
4246
  # originating menu (opts changed while processing)
4057
4247
  selection_opts = if default && menu_blocks.map(&:dname).include?(default)
@@ -4060,26 +4250,24 @@ module MarkdownExec
4060
4250
  @delegate_object
4061
4251
  end
4062
4252
 
4063
- # !!b
4064
4253
  selection_opts.merge!(
4065
4254
  { cycle: @delegate_object[:select_page_cycle],
4066
4255
  per_page: @delegate_object[:select_page_height] }
4067
4256
  )
4068
4257
  selected_option = select_option_with_metadata(prompt_title, menu_items,
4069
4258
  selection_opts)
4070
- # !!b
4071
4259
  determine_block_state(selected_option)
4072
4260
  end
4073
4261
 
4074
4262
  # Handles the core logic for generating the command
4075
4263
  # file's metadata and content.
4076
- def write_command_file(required_lines:, selected:, shell: nil)
4264
+ def write_command_file(required_lines:, blockname:, shell: nil)
4077
4265
  return unless @delegate_object[:save_executed_script]
4078
4266
 
4079
4267
  time_now = Time.now.utc
4080
4268
  @run_state.saved_script_filename =
4081
4269
  SavedAsset.new(
4082
- blockname: selected.pub_name,
4270
+ blockname: blockname,
4083
4271
  exts: '.sh',
4084
4272
  filename: @delegate_object[:filename],
4085
4273
  prefix: @delegate_object[:saved_script_filename_prefix],
@@ -4273,6 +4461,7 @@ module MarkdownExec
4273
4461
  c.expects(:command_execute).with(
4274
4462
  '',
4275
4463
  args: pigeon,
4464
+ erls: {},
4276
4465
  shell: ShellType::BASH
4277
4466
  )
4278
4467
 
@@ -4487,7 +4676,7 @@ module MarkdownExec
4487
4676
  mdoc: @mdoc, selected: @selected, block_source: {}
4488
4677
  )
4489
4678
 
4490
- assert_equal ['code line', 'key="value"'], result
4679
+ assert_equal ['code line', 'key=value'], result
4491
4680
  end
4492
4681
  end
4493
4682
 
@@ -4814,7 +5003,7 @@ module MarkdownExec
4814
5003
  def test_call
4815
5004
  @hd.expects(:history_files).with(nil, filename: '*', path: nil).once
4816
5005
  @hd.execute_block_type_history_ux(filename: '*', link_state: LinkState.new,
4817
- selected: FCB.new(body: []))
5006
+ selected: FCB.new(body: []))
4818
5007
  end
4819
5008
  end
4820
5009