markdown_exec 2.8.0 → 2.8.2

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.
@@ -306,7 +306,7 @@ module HashDelegatorSelf
306
306
  table__hs.each do |table_hs|
307
307
  table_hs.substrings.each do |substrings|
308
308
  substrings.each do |node|
309
- next unless node[:text].class == TrackedString
309
+ next unless node[:text].instance_of?(TrackedString)
310
310
 
311
311
  exceeded_table_cell ||= node[:text].exceeded
312
312
  truncated_table_cell = node[:text].truncated
@@ -606,6 +606,9 @@ module MarkdownExec
606
606
 
607
607
  @process_mutex = Mutex.new
608
608
  @process_cv = ConditionVariable.new
609
+ @dml_link_state = Struct.new(:document_filename, :inherited_lines)
610
+ .new(@delegate_object[:filename], [])
611
+ @dml_menu_blocks = []
609
612
 
610
613
  @p_all_arguments = []
611
614
  @p_options_parsed = []
@@ -934,7 +937,12 @@ module MarkdownExec
934
937
  end
935
938
  end
936
939
 
937
- # private
940
+ def build_menu_options(exit_option, display_mode_option,
941
+ menu_entries, display_format)
942
+ [exit_option,
943
+ display_mode_option,
944
+ *menu_entries.map(&display_format)].compact
945
+ end
938
946
 
939
947
  def build_replacement_dictionary(
940
948
  commands, link_state,
@@ -1017,25 +1025,11 @@ module MarkdownExec
1017
1025
  ]
1018
1026
  end
1019
1027
 
1020
- def neval(export_exec, inherited_code)
1021
- # ww0 export_exec
1022
- # ww0 inherited_code
1023
- code = (inherited_code || []) + [export_exec]
1024
- filespec = generate_temp_filename
1025
- File.write filespec, HashDelegator.join_code_lines(code)
1026
- File.chmod 0o755, filespec
1027
- # ww0 File.read(filespec)
1028
- ret = `#{filespec}`
1029
- File.delete filespec
1030
- ret
1031
- end
1032
-
1033
- # sets ENV
1028
+ # parse YAML body defining the UX for a single variable
1029
+ # set ENV value for the variable and return code lines for the same
1034
1030
  def code_from_ux_block_to_set_environment_variables(
1035
1031
  selected, mdoc, inherited_code: nil, force: true, only_default: false
1036
1032
  )
1037
- # ww0 inherited_code
1038
- # ww0 mdoc
1039
1033
  exit_prompt = @delegate_object[:prompt_filespec_back]
1040
1034
 
1041
1035
  required = mdoc.collect_recursively_required_code(
@@ -1045,104 +1039,106 @@ module MarkdownExec
1045
1039
  block_source: block_source
1046
1040
  )
1047
1041
 
1042
+ # process each ux block in sequence, setting ENV and collecting lines
1048
1043
  code_lines = []
1049
- case data = YAML.load(selected.body.join("\n"))
1050
- when Hash
1051
- export = parse_yaml_of_ux_block(
1052
- data,
1053
- prompt: @delegate_object[:prompt_ux_enter_a_value],
1054
- validate: '^(?<name>[^ ].*)$'
1055
- )
1056
-
1057
- exportable = true
1058
- if only_default
1059
- value = case export.default
1060
- # exec > default
1061
- when :exec
1062
- raise unless export.exec.present?
1044
+ required[:blocks].each do |block|
1045
+ next unless block.type == BlockType::UX
1046
+
1047
+ case data = YAML.load(block.body.join("\n"))
1048
+ when Hash
1049
+ export = parse_yaml_of_ux_block(
1050
+ data,
1051
+ prompt: @delegate_object[:prompt_ux_enter_a_value],
1052
+ validate: '^(?<name>[^ ].*)$'
1053
+ )
1063
1054
 
1064
- n1 = neval(export.exec, (inherited_code || []) + required[:code])
1055
+ # preconditions are variable names that must be set before the UX block is executed.
1056
+ # if any precondition is not set, the sequence is aborted.
1057
+ export.preconditions&.each do |precondition|
1058
+ code_lines.push "[[ -z $#{precondition} ]] && exit 1"
1059
+ end
1065
1060
 
1066
- if export.transform.present?
1067
- if export.transform.is_a? Symbol
1068
- n1.send(export.transform)
1069
- else
1070
- format(
1071
- export.transform,
1072
- NamedCaptureExtractor.extract_named_groups(
1073
- n1, export.validate
1074
- )
1075
- )
1061
+ exportable = true
1062
+ 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
1076
1073
  end
1074
+
1075
+ transform_export_value(output, export)
1076
+ # default
1077
1077
  else
1078
- n1
1078
+ export.default.to_s
1079
1079
  end
1080
+ else
1081
+ value = nil
1080
1082
 
1081
- # default
1082
- else
1083
- export.default.to_s
1084
- end
1085
- else
1086
- caps = nil
1087
- value = nil
1088
-
1089
- # exec > allowed
1090
- if export.exec
1091
- value = neval(export.exec, (inherited_code || []) + required[:code])
1092
- caps = NamedCaptureExtractor.extract_named_groups(value,
1093
- export.validate)
1094
-
1095
- # allowed > prompt
1096
- elsif export.allowed && export.allowed.count.positive?
1097
- case (choice = prompt_select_code_filename(
1098
- [exit_prompt] + export.allowed,
1099
- string: export.prompt,
1100
- color_sym: :prompt_color_after_script_execution
1101
- ))
1102
- when exit_prompt
1103
- exportable = false
1104
- else
1105
- value = choice
1106
- caps = NamedCaptureExtractor.extract_named_groups(value,
1107
- export.validate)
1108
- end
1109
-
1110
- # prompt > default
1111
- elsif export.prompt.present?
1112
- begin
1113
- while true
1114
- print "#{export.prompt} [#{export.default}]: "
1115
- value = gets.chomp
1116
- value = export.default.to_s if value.empty?
1117
- caps = NamedCaptureExtractor.extract_named_groups(value,
1118
- export.validate)
1119
- break if caps
1083
+ # exec > allowed
1084
+ if export.exec
1085
+ value = export_exec_with_code(
1086
+ export, inherited_code, code_lines, required
1087
+ )
1088
+ if value == :invalidated
1089
+ return :ux_exec_prohibited
1090
+ end
1120
1091
 
1121
- # invalid input, retry
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
1122
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
1123
1120
  end
1124
- rescue Interrupt
1125
- exportable = false
1121
+
1122
+ # default
1123
+ else
1124
+ value = export.default
1126
1125
  end
1127
1126
 
1128
- # default
1129
- else
1130
- value = export.default
1127
+ if exportable
1128
+ value = transform_export_value(value, export)
1129
+ end
1131
1130
  end
1132
1131
 
1133
- if exportable && export.transform.present?
1134
- value = format(export.transform, caps)
1132
+ if exportable
1133
+ ENV[export.name] = value.to_s
1134
+ code_lines.push code_line_safe_assign(export.name, value,
1135
+ force: force)
1135
1136
  end
1137
+ else
1138
+ raise "Invalid data type: #{data.inspect}"
1136
1139
  end
1137
-
1138
- if exportable
1139
- ENV[export.name] = value.to_s
1140
- code_lines.push code_line_safe_assign(export.name, value,
1141
- force: force)
1142
- end
1143
- else
1144
- raise "Invalid data type: #{data.inspect}"
1145
1140
  end
1141
+
1146
1142
  code_lines
1147
1143
  end
1148
1144
 
@@ -1167,7 +1163,6 @@ module MarkdownExec
1167
1163
 
1168
1164
  def code_line_safe_assign(name, value, force:)
1169
1165
  if force
1170
- # ww0 value
1171
1166
  "#{name}=#{Shellwords.escape(value)}"
1172
1167
  else
1173
1168
  "[[ -z $#{name} ]] && #{name}=#{Shellwords.escape(value)}"
@@ -1216,7 +1211,8 @@ module MarkdownExec
1216
1211
  command_execute_in_process(
1217
1212
  args: args, command: command,
1218
1213
  erls: erls,
1219
- filename: @delegate_object[:filename], shell: shell
1214
+ filename: @delegate_object[:filename],
1215
+ shell: shell
1220
1216
  )
1221
1217
  end
1222
1218
 
@@ -1280,7 +1276,8 @@ module MarkdownExec
1280
1276
  )
1281
1277
  execute_command_with_streams(
1282
1278
  [shell, '-c', command,
1283
- @delegate_object[:filename], *args]
1279
+ @delegate_object[:filename],
1280
+ *args]
1284
1281
  )
1285
1282
  end
1286
1283
 
@@ -1981,19 +1978,21 @@ module MarkdownExec
1981
1978
  block_source: block_source
1982
1979
  )
1983
1980
 
1984
- # if the same menu is being displayed, collect the display name
1985
- # of the selected menu item for use as the default item
1986
- [lfls.link_state,
1987
- lfls.load_file == LoadFile::LOAD ? nil : selected.dname,
1988
- # 2024-08-22 true to quit
1989
- lfls.load_file == LoadFile::EXIT]
1981
+ # dname is not fixed for some block types, use block id
1982
+ if lfls.load_file != LoadFile::LOAD
1983
+ block_selection = BlockSelection.new(selected.id)
1984
+ end
1985
+
1986
+ { link_state: lfls.link_state,
1987
+ block_selection: block_selection,
1988
+ quit: lfls.load_file == LoadFile::EXIT }
1990
1989
  end
1991
1990
 
1992
1991
  def execute_block_in_state(block_name)
1993
1992
  @dml_block_state = block_state_for_name_from_cli(block_name)
1994
1993
  dump_and_warn_block_state(name: block_name,
1995
1994
  selected: @dml_block_state.block)
1996
- @dml_link_state, @dml_menu_default_dname, quit =
1995
+ next_block_state =
1997
1996
  execute_block_for_state_and_name(
1998
1997
  selected: @dml_block_state.block,
1999
1998
  mdoc: @dml_mdoc,
@@ -2005,7 +2004,10 @@ module MarkdownExec
2005
2004
  )
2006
2005
  }
2007
2006
  )
2008
- :break if quit
2007
+
2008
+ @dml_link_state = next_block_state[:link_state]
2009
+ @dml_block_selection = next_block_state[:block_selection]
2010
+ :break if next_block_state[:quit]
2009
2011
  end
2010
2012
 
2011
2013
  def execute_block_type_history_ux(
@@ -2152,6 +2154,7 @@ module MarkdownExec
2152
2154
  block_data['glob'] || glob
2153
2155
  )
2154
2156
  )
2157
+ dirs.sort_by! { |f| File.mtime(f) }.reverse!
2155
2158
 
2156
2159
  if !contains_glob?(block_data['directory']) &&
2157
2160
  !contains_glob?(block_data['glob'])
@@ -2160,24 +2163,24 @@ module MarkdownExec
2160
2163
  else
2161
2164
  warn 'No matching file found.'
2162
2165
  end
2163
- elsif selected_option = select_option_with_metadata(
2166
+ elsif (selected_option = select_option_with_metadata(
2164
2167
  prompt_title,
2165
- [exit_prompt] + dirs.sort.map do |file|
2166
- { name: format(
2167
- block_data['view'] || view,
2168
- NamedCaptureExtractor.extract_named_group_match_data(
2169
- file.match(
2170
- Regexp.new(block_data['filename_pattern'] ||
2171
- filename_pattern)
2172
- )
2173
- )
2174
- ),
2168
+ [exit_prompt] + dirs.map do |file|
2169
+ { name:
2170
+ format(
2171
+ block_data['view'] || view,
2172
+ NamedCaptureExtractor.extract_named_group_match_data(
2173
+ file.match(
2174
+ Regexp.new(block_data['filename_pattern'] || filename_pattern)
2175
+ )
2176
+ )
2177
+ ),
2175
2178
  oname: file }
2176
2179
  end,
2177
2180
  menu_options.merge(
2178
2181
  cycle: true
2179
2182
  )
2180
- )
2183
+ ))
2181
2184
  if selected_option.dname != exit_prompt
2182
2185
  File.readlines(selected_option.oname, chomp: true)
2183
2186
  end
@@ -2326,50 +2329,34 @@ module MarkdownExec
2326
2329
  )
2327
2330
  # repeat select+display until user exits
2328
2331
 
2329
- pause_now = false
2330
- row_attrib = :row
2331
- loop do
2332
- if pause_now && (prompt_select_continue == MenuState::EXIT)
2333
- break
2334
- end
2335
-
2336
- # menu with Back and Facet options at top
2337
- case (name = prompt_select_code_filename(
2338
- [exit_prompt,
2339
- @delegate_object[:prompt_filespec_facet]] +
2340
- files_table_rows.map(&row_attrib),
2341
- string: @delegate_object[:prompt_select_history_file],
2342
- color_sym: :prompt_color_after_script_execution
2343
- ))
2344
- when exit_prompt
2345
- break
2346
- when @delegate_object[:prompt_filespec_facet]
2347
- row_attrib = row_attrib == :row ? :file : :row
2348
- pause_now = false
2349
- else
2350
- file = files_table_rows.select { |ftr| ftr.row == name }&.first
2351
- info = file_info(file.file)
2352
- stream.puts "#{file.file} - #{info[:lines]} lines / " \
2353
- "#{info[:size]} bytes"
2354
- stream.puts(
2355
- File.readlines(file.file,
2356
- chomp: false).map.with_index do |line, ind|
2357
- format(' %s. %s',
2358
- AnsiString.new(format('% 4d', ind + 1)).send(:violet),
2359
- line)
2360
- end
2361
- )
2362
- pause_now = pause_refresh
2363
- end
2332
+ interactive_menu_with_display_modes(
2333
+ files_table_rows,
2334
+ display_formats: %i[row file],
2335
+ display_mode_option: @delegate_object[:prompt_filespec_facet],
2336
+ exit_option: exit_prompt,
2337
+ menu_title: @delegate_object[:prompt_select_history_file],
2338
+ pause_after_selection: pause_refresh
2339
+ ) do |file|
2340
+ info = file_info(file.file)
2341
+ stream.puts "#{file.file} - #{info[:lines]} lines / " \
2342
+ "#{info[:size]} bytes"
2343
+ stream.puts(
2344
+ File.readlines(file.file,
2345
+ chomp: false).map.with_index do |line, ind|
2346
+ format(' %s. %s',
2347
+ AnsiString.new(format('% 4d', ind + 1)).send(:violet),
2348
+ line)
2349
+ end
2350
+ )
2364
2351
  end
2365
2352
  end
2366
2353
 
2367
2354
  def execute_inherited_save(
2368
2355
  code_lines: @dml_link_state.inherited_lines
2369
2356
  )
2370
- return unless save_filespec = save_filespec_from_expression(
2371
- document_name_in_glob_as_file_name
2372
- )
2357
+ return unless (save_filespec = save_filespec_from_expression)
2358
+
2359
+ document_name_in_glob_as_file_name
2373
2360
 
2374
2361
  unless write_file_with_directory_creation(
2375
2362
  content: HashDelegator.join_code_lines(code_lines),
@@ -2425,6 +2412,27 @@ module MarkdownExec
2425
2412
  post_execution_process
2426
2413
  end
2427
2414
 
2415
+ def execute_temporary_script(script_code, additional_code = [])
2416
+ full_code = (additional_code || []) + [script_code]
2417
+
2418
+ Tempfile.create('script_exec') do |temp_file|
2419
+ temp_file.write(HashDelegator.join_code_lines(full_code))
2420
+ temp_file.flush
2421
+ File.chmod(0o755, temp_file.path)
2422
+
2423
+ output = `#{temp_file.path}`
2424
+
2425
+ if $?.exitstatus != 0
2426
+ return :invalidated
2427
+ end
2428
+
2429
+ output
2430
+ end
2431
+ rescue StandardError => err
2432
+ warn "Error executing script: #{err.message}"
2433
+ nil
2434
+ end
2435
+
2428
2436
  def expand_blocks_with_replacements(
2429
2437
  menu_blocks, replacements, exclude_types: [BlockType::SHELL]
2430
2438
  )
@@ -2471,6 +2479,18 @@ module MarkdownExec
2471
2479
  expand_blocks_with_replacements(blocks, replacements)
2472
2480
  end
2473
2481
 
2482
+ def export_exec_with_code(export, inherited_code, code_lines, required)
2483
+ value = execute_temporary_script(
2484
+ export.exec,
2485
+ (inherited_code || []) +
2486
+ code_lines + required[:code]
2487
+ )
2488
+ if value == :invalidated
2489
+ warn "A value must exist for: #{export.preconditions.join(', ')}"
2490
+ end
2491
+ value
2492
+ end
2493
+
2474
2494
  # Retrieves a specific data symbol from the delegate object,
2475
2495
  # converts it to a string, and applies a color style
2476
2496
  # based on the specified color symbol.
@@ -2583,7 +2603,7 @@ module MarkdownExec
2583
2603
  # commands to echo variables
2584
2604
  #
2585
2605
  commands = {}
2586
- variable_counts.each do |variable, _count|
2606
+ variable_counts.each_key do |variable|
2587
2607
  command = format(echo_format, variable)
2588
2608
  commands[variable] = command
2589
2609
  end
@@ -2640,7 +2660,6 @@ module MarkdownExec
2640
2660
  end
2641
2661
 
2642
2662
  def history_files(
2643
- link_state,
2644
2663
  direction: :reverse,
2645
2664
  filename: nil,
2646
2665
  home: Dir.pwd,
@@ -2679,6 +2698,56 @@ module MarkdownExec
2679
2698
  }
2680
2699
  end
2681
2700
 
2701
+ def interactive_menu_with_display_modes(
2702
+ menu_entries,
2703
+ display_formats:,
2704
+ display_mode_option:,
2705
+ exit_option:,
2706
+ menu_title:,
2707
+ pause_after_selection:
2708
+ )
2709
+ pause_menu = false
2710
+ current_display_format = display_formats.first
2711
+
2712
+ loop do
2713
+ break if pause_menu && (prompt_select_continue == MenuState::EXIT)
2714
+
2715
+ menu_options = build_menu_options(
2716
+ exit_option, display_mode_option,
2717
+ menu_entries, current_display_format
2718
+ )
2719
+
2720
+ selection = prompt_select_code_filename(
2721
+ menu_options,
2722
+ string: menu_title,
2723
+ color_sym: :prompt_color_after_script_execution
2724
+ )
2725
+
2726
+ case selection
2727
+ when exit_option
2728
+ break
2729
+ when display_mode_option
2730
+ current_display_format = next_item(
2731
+ display_formats, current_display_format
2732
+ )
2733
+ pause_menu = false
2734
+ else
2735
+ handle_selection(menu_entries, selection,
2736
+ current_display_format) do |item|
2737
+ yield item if block_given?
2738
+ end
2739
+ pause_menu = pause_after_selection
2740
+ end
2741
+ end
2742
+ end
2743
+
2744
+ def handle_selection(menu_entries, selection, current_display_format)
2745
+ selected_item = menu_entries.find do |entry|
2746
+ entry.send(current_display_format) == selection
2747
+ end
2748
+ yield selected_item if selected_item
2749
+ end
2750
+
2682
2751
  # Iterates through blocks in a file, applying the provided block to each line.
2683
2752
  # The iteration only occurs if the file exists.
2684
2753
  # @yield [Symbol] :filter Yields to obtain selected messages for processing.
@@ -2903,19 +2972,24 @@ module MarkdownExec
2903
2972
  @ux_most_recent_filename = @delegate_object[:filename]
2904
2973
 
2905
2974
  (blocks.each.with_object([]) do |block, merged_options|
2906
- merged_options.push(
2907
- code_from_ux_block_to_set_environment_variables(
2908
- block,
2909
- mdoc,
2910
- force: @delegate_object[:ux_auto_load_force_default],
2911
- only_default: true
2912
- )
2975
+ code = code_from_ux_block_to_set_environment_variables(
2976
+ block,
2977
+ mdoc,
2978
+ force: @delegate_object[:ux_auto_load_force_default],
2979
+ only_default: true
2913
2980
  )
2981
+ if code == :ux_exec_prohibited
2982
+ merged_options
2983
+ else
2984
+ merged_options.push(code)
2985
+ end
2914
2986
  end).to_a
2915
2987
  end
2916
2988
 
2917
- def load_auto_vars_block(all_blocks,
2918
- block_name: @delegate_object[:document_load_vars_block_name])
2989
+ def load_auto_vars_block(
2990
+ all_blocks,
2991
+ block_name: @delegate_object[:document_load_vars_block_name]
2992
+ )
2919
2993
  unless block_name.present? &&
2920
2994
  @vars_most_recent_filename != @delegate_object[:filename]
2921
2995
  return
@@ -3056,7 +3130,7 @@ module MarkdownExec
3056
3130
 
3057
3131
  # load document shell block
3058
3132
  #
3059
- if code_lines = load_document_shell_block(all_blocks, mdoc: mdoc)
3133
+ if (code_lines = load_document_shell_block(all_blocks, mdoc: mdoc))
3060
3134
  next_state_set_code(nil, link_state, code_lines)
3061
3135
  link_state.inherited_lines = code_lines
3062
3136
  reload_blocks = true
@@ -3064,7 +3138,7 @@ module MarkdownExec
3064
3138
 
3065
3139
  # load document ux block
3066
3140
  #
3067
- if code_lines = load_auto_ux_block(all_blocks, mdoc)
3141
+ if (code_lines = load_auto_ux_block(all_blocks, mdoc))
3068
3142
  new_code = HashDelegator.code_merge(link_state.inherited_lines,
3069
3143
  code_lines)
3070
3144
  next_state_set_code(nil, link_state, new_code)
@@ -3074,7 +3148,7 @@ module MarkdownExec
3074
3148
 
3075
3149
  # load document vars block
3076
3150
  #
3077
- if code_lines = load_auto_vars_block(all_blocks)
3151
+ if (code_lines = load_auto_vars_block(all_blocks))
3078
3152
  new_code = HashDelegator.code_merge(link_state.inherited_lines,
3079
3153
  code_lines)
3080
3154
  next_state_set_code(nil, link_state, new_code)
@@ -3209,11 +3283,21 @@ module MarkdownExec
3209
3283
  end
3210
3284
  end
3211
3285
 
3286
+ def next_item(list, current_item)
3287
+ index = list.index(current_item)
3288
+ return nil unless index # Return nil if the item is not in the list
3289
+
3290
+ list[(index + 1) % list.size] # Get the next item, wrap around if at the end
3291
+ end
3292
+
3212
3293
  def next_state_append_code(selected, link_state, code_lines)
3213
3294
  next_state_set_code(
3214
3295
  selected,
3215
3296
  link_state,
3216
- HashDelegator.code_merge(link_state&.inherited_lines, code_lines)
3297
+ HashDelegator.code_merge(
3298
+ link_state&.inherited_lines,
3299
+ code_lines.is_a?(Array) ? code_lines : [] # no code for :ux_exec_prohibited
3300
+ )
3217
3301
  )
3218
3302
  end
3219
3303
 
@@ -3355,8 +3439,8 @@ module MarkdownExec
3355
3439
  #
3356
3440
  # @param all_blocks [Array<Hash>] The list of blocks from the file.
3357
3441
  def select_blocks(menu_blocks)
3358
- menu_blocks.select do |fcb|
3359
- !Filter.prepared_not_in_menu?(
3442
+ menu_blocks.reject do |fcb|
3443
+ Filter.prepared_not_in_menu?(
3360
3444
  @delegate_object,
3361
3445
  fcb,
3362
3446
  %i[block_name_include_match block_name_wrapper_match]
@@ -3605,16 +3689,16 @@ module MarkdownExec
3605
3689
 
3606
3690
  case @delegate_object[:publish_document_file_mode]
3607
3691
  when 'append'
3608
- File.write(pipe_path, message + "\n", mode: 'a')
3692
+ File.write(pipe_path, "#{message}\n", mode: 'a')
3609
3693
  when 'fifo'
3610
3694
  unless @vux_pipe_open
3611
3695
  unless File.exist?(pipe_path)
3612
- FileUtils.mkfifo(pipe_path)
3696
+ File.mkfifo(pipe_path)
3613
3697
  @vux_pipe_created = pipe_path
3614
3698
  end
3615
3699
  @vux_pipe_open = File.open(pipe_path, 'w')
3616
3700
  end
3617
- @vux_pipe_open.puts(message + "\n")
3701
+ @vux_pipe_open.puts("#{message}\n")
3618
3702
  @vux_pipe_open.flush
3619
3703
  when 'write'
3620
3704
  File.write(pipe_path, message)
@@ -3641,7 +3725,6 @@ module MarkdownExec
3641
3725
  regexp: @delegate_object[:saved_asset_match]
3642
3726
  )
3643
3727
  history_files(
3644
- @dml_link_state,
3645
3728
  filename:
3646
3729
  if asset.present?
3647
3730
  saved_asset_filename(asset, @dml_link_state)
@@ -3656,7 +3739,8 @@ module MarkdownExec
3656
3739
  end
3657
3740
 
3658
3741
  saved_asset = saved_asset_for_history(
3659
- file: file, form: form,
3742
+ file: file,
3743
+ form: form,
3660
3744
  match_info: $LAST_MATCH_INFO
3661
3745
  )
3662
3746
  saved_asset == :break ? nil : saved_asset
@@ -3905,7 +3989,7 @@ module MarkdownExec
3905
3989
 
3906
3990
  def screen_width
3907
3991
  width = @delegate_object[:screen_width]
3908
- if width && width.positive?
3992
+ if width&.positive?
3909
3993
  width
3910
3994
  else
3911
3995
  @delegate_object[:console_width]
@@ -3922,7 +4006,7 @@ module MarkdownExec
3922
4006
  end
3923
4007
 
3924
4008
  def select_document_if_multiple(options, files, prompt:)
3925
- return files if files.class == String
4009
+ return files if files.instance_of?(String)
3926
4010
  return files[0] if (count = files.count) == 1
3927
4011
 
3928
4012
  return unless count >= 2
@@ -3940,7 +4024,11 @@ module MarkdownExec
3940
4024
 
3941
4025
  # Presents a TTY prompt to select an option or exit,
3942
4026
  # returns metadata including option and selected
3943
- def select_option_with_metadata(prompt_text, menu_items, opts = {})
4027
+ def select_option_with_metadata(
4028
+ prompt_text, menu_items, opts = {}, menu_blocks: nil
4029
+ )
4030
+ @dml_menu_blocks = menu_blocks if menu_blocks
4031
+
3944
4032
  ## configure to environment
3945
4033
  #
3946
4034
  register_console_attributes(opts)
@@ -3985,23 +4073,16 @@ module MarkdownExec
3985
4073
  return
3986
4074
  end
3987
4075
 
3988
- selected = menu_items.find do |item|
4076
+ selected = @dml_menu_blocks.find do |item|
3989
4077
  if item.instance_of?(Hash)
3990
4078
  [item[:id], item[:name], item[:dname]].include?(selection)
3991
4079
  elsif item.instance_of?(MarkdownExec::FCB)
3992
- item.dname == selection || item.id == selection
4080
+ item.id == selection
3993
4081
  else
3994
4082
  item == selection
3995
4083
  end
3996
4084
  end
3997
4085
 
3998
- # new FCB if selected is not an object
3999
- if selected.instance_of?(String)
4000
- selected = FCB.new(dname: selected)
4001
- elsif selected.instance_of?(Hash)
4002
- selected = FCB.new(selected)
4003
- end
4004
-
4005
4086
  unless selected
4006
4087
  HashDelegator.error_handler('select_option_with_metadata',
4007
4088
  error: 'menu item not found')
@@ -4111,7 +4192,7 @@ module MarkdownExec
4111
4192
  disabled = if fcb_title_groups.fetch(:type, '') == BlockType::YAML
4112
4193
  TtyMenu::DISABLE
4113
4194
  else
4114
- nil
4195
+ TtyMenu::ENABLE
4115
4196
  end
4116
4197
 
4117
4198
  MarkdownExec::FCB.new(
@@ -4152,6 +4233,21 @@ module MarkdownExec
4152
4233
  HashDelegator.apply_color_from_hash(string, @delegate_object, color_sym)
4153
4234
  end
4154
4235
 
4236
+ def transform_export_value(value, export)
4237
+ return value unless export.transform.present?
4238
+
4239
+ if export.transform.is_a? Symbol
4240
+ value.send(export.transform)
4241
+ else
4242
+ format(
4243
+ export.transform,
4244
+ NamedCaptureExtractor.extract_named_groups(
4245
+ value, export.validate
4246
+ )
4247
+ )
4248
+ end
4249
+ end
4250
+
4155
4251
  ##
4156
4252
  # Processes an individual line within a loop, updating headings
4157
4253
  # and handling fenced code blocks.
@@ -4237,11 +4333,11 @@ module MarkdownExec
4237
4333
  @delegate_object.merge!(options)
4238
4334
  end
4239
4335
 
4240
- def vux_await_user_selection
4336
+ def vux_await_user_selection(prior_answer: @dml_block_selection)
4241
4337
  @dml_block_state = load_cli_or_user_selected_block(
4242
4338
  all_blocks: @dml_blocks_in_file,
4243
4339
  menu_blocks: @dml_menu_blocks,
4244
- prior_answer: @dml_menu_default_dname
4340
+ prior_answer: prior_answer
4245
4341
  )
4246
4342
  if !@dml_block_state
4247
4343
  # HashDelegator.error_handler('block_state missing', { abort: true })
@@ -4318,7 +4414,7 @@ module MarkdownExec
4318
4414
 
4319
4415
  when formatted_choice_ostructs[:history].pub_name
4320
4416
  debounce_reset
4321
- return :break unless files_table_rows = vux_history_files_table_rows
4417
+ return :break unless (files_table_rows = vux_history_files_table_rows)
4322
4418
 
4323
4419
  execute_history_select(files_table_rows, stream: $stderr)
4324
4420
  return :break if pause_user_exit
@@ -4395,7 +4491,7 @@ module MarkdownExec
4395
4491
  @dml_link_state.block_name.present?
4396
4492
  @cli_block_name = @dml_link_state.block_name
4397
4493
  @dml_now_using_cli = @run_state.source.block_name_from_cli
4398
- @dml_menu_default_dname = nil
4494
+ @dml_block_selection = nil
4399
4495
  @dml_block_state = SelectedBlockMenuState.new
4400
4496
  @doc_saved_lines_files = []
4401
4497
 
@@ -4444,9 +4540,9 @@ module MarkdownExec
4444
4540
  end
4445
4541
 
4446
4542
  def vux_load_inherited
4447
- return unless filespec = load_filespec_from_expression(
4543
+ return unless (filespec = load_filespec_from_expression(
4448
4544
  document_name_in_glob_as_file_name
4449
- )
4545
+ ))
4450
4546
 
4451
4547
  @dml_link_state.inherited_lines_append(
4452
4548
  File.readlines(filespec, chomp: true)
@@ -4565,7 +4661,6 @@ module MarkdownExec
4565
4661
  )
4566
4662
  if @delegate_object[:menu_for_history]
4567
4663
  history_files(
4568
- @dml_link_state,
4569
4664
  filename: saved_asset_filename(@delegate_object[:filename],
4570
4665
  @dml_link_state),
4571
4666
  path: @delegate_object[:saved_script_folder]
@@ -4713,7 +4808,9 @@ module MarkdownExec
4713
4808
  else
4714
4809
  # puts "? - Select a block to execute (or type #{$texit}
4715
4810
  # to exit):"
4716
- return :break if vux_await_user_selection == :break
4811
+ return :break if vux_await_user_selection(
4812
+ prior_answer: @dml_block_selection
4813
+ ) == :break
4717
4814
  return :break if @dml_block_state.block.nil? # no block matched
4718
4815
  end
4719
4816
  # puts "! - Executing block: #{data}"
@@ -4762,9 +4859,17 @@ module MarkdownExec
4762
4859
  menu_blocks.find do |block|
4763
4860
  block.dname.include?(prior_answer)
4764
4861
  end&.name
4765
- when Struct
4766
- prior_answer.index || prior_answer.name
4862
+ when Struct, MarkdownExec::FCB
4863
+ if prior_answer.id
4864
+ # when switching documents, the prior answer will not be found
4865
+ (menu_blocks.find_index do |block|
4866
+ block[:id] == prior_answer.id
4867
+ end || 0) + 1
4868
+ else
4869
+ prior_answer.index || prior_answer.name
4870
+ end
4767
4871
  end
4872
+
4768
4873
  # prior_answer value may not match if color is different from
4769
4874
  # originating menu (opts changed while processing)
4770
4875
  selection_opts = if selected_answer
@@ -5095,13 +5200,12 @@ module MarkdownExec
5095
5200
 
5096
5201
  class TestHashDelegatorAppendDivider < Minitest::Test
5097
5202
  def setup
5098
- @hd = HashDelegator.new
5099
- @hd.instance_variable_set(:@delegate_object, {
5100
- menu_divider_format: 'Format',
5101
- menu_initial_divider: 'Initial Divider',
5102
- menu_final_divider: 'Final Divider',
5103
- menu_divider_color: :color
5104
- })
5203
+ @hd = HashDelegator.new(
5204
+ menu_divider_color: :color,
5205
+ menu_divider_format: 'Format',
5206
+ menu_final_divider: 'Final Divider',
5207
+ menu_initial_divider: 'Initial Divider'
5208
+ )
5105
5209
  @hd.stubs(:string_send_color).returns('Formatted Divider')
5106
5210
  HashDelegator.stubs(:safeval).returns('Safe Value')
5107
5211
  end
@@ -5123,7 +5227,7 @@ module MarkdownExec
5123
5227
  end
5124
5228
 
5125
5229
  def test_append_divider_without_format
5126
- @hd.instance_variable_set(:@delegate_object, {})
5230
+ @hd = HashDelegator.new
5127
5231
  menu_blocks = []
5128
5232
  @hd.append_divider(menu_blocks: menu_blocks, position: :initial)
5129
5233
 
@@ -5161,7 +5265,6 @@ module MarkdownExec
5161
5265
  @hd = HashDelegator.new
5162
5266
  @hd.stubs(:iter_blocks_from_nested_files).yields(:blocks, FCB.new)
5163
5267
  @hd.stubs(:create_and_add_chrome_blocks)
5164
- @hd.instance_variable_set(:@delegate_object, {})
5165
5268
  HashDelegator.stubs(:error_handler)
5166
5269
  end
5167
5270
 
@@ -5172,7 +5275,7 @@ module MarkdownExec
5172
5275
  end
5173
5276
 
5174
5277
  def test_blocks_from_nested_files_with_no_chrome
5175
- @hd.instance_variable_set(:@delegate_object, { no_chrome: true })
5278
+ @hd = HashDelegator.new(no_chrome: true)
5176
5279
  @hd.expects(:create_and_add_chrome_blocks).never
5177
5280
 
5178
5281
  result = @hd.blocks_from_nested_files
@@ -5184,7 +5287,6 @@ module MarkdownExec
5184
5287
  class TestHashDelegatorCollectRequiredCodeLines < Minitest::Test
5185
5288
  def setup
5186
5289
  @hd = HashDelegator.new
5187
- @hd.instance_variable_set(:@delegate_object, {})
5188
5290
  @mdoc = mock('YourMDocClass')
5189
5291
  @selected = FCB.new(
5190
5292
  body: ['key: value'],
@@ -5210,15 +5312,13 @@ module MarkdownExec
5210
5312
  class TestHashDelegatorCommandOrUserSelectedBlock < Minitest::Test
5211
5313
  def setup
5212
5314
  @hd = HashDelegator.new
5213
- @hd.instance_variable_set(:@delegate_object, {})
5214
5315
  HashDelegator.stubs(:error_handler)
5215
5316
  @hd.stubs(:wait_for_user_selected_block)
5216
5317
  end
5217
5318
 
5218
5319
  def test_command_selected_block
5219
5320
  all_blocks = [{ oname: 'block1' }, { oname: 'block2' }]
5220
- @hd.instance_variable_set(:@delegate_object,
5221
- { block_name: 'block1' })
5321
+ @hd = HashDelegator.new(block_name: 'block1')
5222
5322
 
5223
5323
  result = @hd.load_cli_or_user_selected_block(all_blocks: all_blocks)
5224
5324
 
@@ -5247,10 +5347,10 @@ module MarkdownExec
5247
5347
 
5248
5348
  class TestHashDelegatorCountBlockInFilename < Minitest::Test
5249
5349
  def setup
5250
- @hd = HashDelegator.new
5251
- @hd.instance_variable_set(:@delegate_object,
5252
- { fenced_start_and_end_regex: '^```',
5253
- filename: '/path/to/file' })
5350
+ @hd = HashDelegator.new(
5351
+ fenced_start_and_end_regex: '^```',
5352
+ filename: '/path/to/file'
5353
+ )
5254
5354
  @hd.stubs(:cfile).returns(mock('cfile'))
5255
5355
  end
5256
5356
 
@@ -5359,7 +5459,6 @@ module MarkdownExec
5359
5459
  def setup
5360
5460
  @hd = HashDelegator.new
5361
5461
  @hd.instance_variable_set(:@fout, mock('fout'))
5362
- @hd.instance_variable_set(:@delegate_object, {})
5363
5462
  @hd.stubs(:string_send_color)
5364
5463
  end
5365
5464
 
@@ -5381,7 +5480,6 @@ module MarkdownExec
5381
5480
  class TestHashDelegatorFetchColor < Minitest::Test
5382
5481
  def setup
5383
5482
  @hd = HashDelegator.new
5384
- @hd.instance_variable_set(:@delegate_object, {})
5385
5483
  end
5386
5484
 
5387
5485
  def test_fetch_color_with_valid_data
@@ -5414,7 +5512,6 @@ module MarkdownExec
5414
5512
  class TestHashDelegatorFormatReferencesSendColor < Minitest::Test
5415
5513
  def setup
5416
5514
  @hd = HashDelegator.new
5417
- @hd.instance_variable_set(:@delegate_object, {})
5418
5515
  end
5419
5516
 
5420
5517
  def test_format_references_send_color_with_valid_data
@@ -5519,7 +5616,6 @@ module MarkdownExec
5519
5616
  # )
5520
5617
 
5521
5618
  # def history_files(
5522
- # link_state,
5523
5619
  # direction: :reverse,
5524
5620
  # filename: nil,
5525
5621
  # home: Dir.pwd,
@@ -5528,9 +5624,10 @@ module MarkdownExec
5528
5624
  # )
5529
5625
 
5530
5626
  def test_call
5531
- @hd.expects(:history_files).with(nil, filename: '*', path: nil).once
5532
- @hd.execute_block_type_history_ux(filename: '*', link_state: LinkState.new,
5533
- selected: FCB.new(body: []))
5627
+ @hd.expects(:history_files).with(filename: '*', path: nil).once
5628
+ @hd.execute_block_type_history_ux(
5629
+ filename: '*', link_state: LinkState.new, selected: FCB.new(body: [])
5630
+ )
5534
5631
  end
5535
5632
  end
5536
5633
 
@@ -5615,11 +5712,9 @@ module MarkdownExec
5615
5712
 
5616
5713
  class TestHashDelegatorHandleStream < Minitest::Test
5617
5714
  def setup
5618
- @hd = HashDelegator.new
5715
+ @hd = HashDelegator.new(output_stdout: true)
5619
5716
  @hd.instance_variable_set(:@run_state,
5620
5717
  OpenStruct.new(files: StreamsOut.new))
5621
- @hd.instance_variable_set(:@delegate_object,
5622
- { output_stdout: true })
5623
5718
  end
5624
5719
 
5625
5720
  def test_handle_stream
@@ -5651,9 +5746,7 @@ module MarkdownExec
5651
5746
 
5652
5747
  class TestHashDelegatorIterBlocksFromNestedFiles < Minitest::Test
5653
5748
  def setup
5654
- @hd = HashDelegator.new
5655
- @hd.instance_variable_set(:@delegate_object,
5656
- { filename: 'test.md' })
5749
+ @hd = HashDelegator.new(filename: 'test.md')
5657
5750
  @hd.stubs(:check_file_existence).with('test.md').returns(true)
5658
5751
  @hd.stubs(:initial_state).returns({})
5659
5752
  @hd.stubs(:cfile).returns(Minitest::Mock.new)
@@ -5682,12 +5775,11 @@ module MarkdownExec
5682
5775
 
5683
5776
  class TestHashDelegatorMenuChromeColoredOption < Minitest::Test
5684
5777
  def setup
5685
- @hd = HashDelegator.new
5686
- @hd.instance_variable_set(:@delegate_object, {
5687
- menu_option_back_name: 'Back',
5688
- menu_chrome_color: :red,
5689
- menu_chrome_format: '-- %s --'
5690
- })
5778
+ @hd = HashDelegator.new(
5779
+ menu_chrome_color: :red,
5780
+ menu_chrome_format: '-- %s --',
5781
+ menu_option_back_name: 'Back'
5782
+ )
5691
5783
  @hd.stubs(:menu_chrome_formatted_option)
5692
5784
  .with(:menu_option_back_name).returns('-- Back --')
5693
5785
  @hd.stubs(:string_send_color)
@@ -5701,8 +5793,9 @@ module MarkdownExec
5701
5793
  end
5702
5794
 
5703
5795
  def test_menu_chrome_colored_option_without_color
5704
- @hd.instance_variable_set(:@delegate_object,
5705
- { menu_option_back_name: 'Back' })
5796
+ @hd = HashDelegator.new(menu_option_back_name: 'Back')
5797
+ @hd.stubs(:menu_chrome_formatted_option)
5798
+ .with(:menu_option_back_name).returns('-- Back --')
5706
5799
  assert_equal '-- Back --',
5707
5800
  @hd.menu_chrome_colored_option(:menu_option_back_name)
5708
5801
  end
@@ -5710,11 +5803,10 @@ module MarkdownExec
5710
5803
 
5711
5804
  class TestHashDelegatorMenuChromeOption < Minitest::Test
5712
5805
  def setup
5713
- @hd = HashDelegator.new
5714
- @hd.instance_variable_set(:@delegate_object, {
5715
- menu_option_back_name: "'Back'",
5716
- menu_chrome_format: '-- %s --'
5717
- })
5806
+ @hd = HashDelegator.new(
5807
+ menu_chrome_format: '-- %s --',
5808
+ menu_option_back_name: "'Back'"
5809
+ )
5718
5810
  HashDelegator.stubs(:safeval).with("'Back'").returns('Back')
5719
5811
  end
5720
5812
 
@@ -5724,8 +5816,7 @@ module MarkdownExec
5724
5816
  end
5725
5817
 
5726
5818
  def test_menu_chrome_formatted_option_without_format
5727
- @hd.instance_variable_set(:@delegate_object,
5728
- { menu_option_back_name: "'Back'" })
5819
+ @hd = HashDelegator.new(menu_option_back_name: "'Back'")
5729
5820
  assert_equal 'Back',
5730
5821
  @hd.menu_chrome_formatted_option(:menu_option_back_name)
5731
5822
  end
@@ -5733,10 +5824,12 @@ module MarkdownExec
5733
5824
 
5734
5825
  class TestHashDelegatorStartFencedBlock < Minitest::Test
5735
5826
  def setup
5736
- @hd = HashDelegator.new({
5737
- block_name_wrapper_match: 'WRAPPER_REGEX',
5738
- block_calls_scan: 'CALLS_REGEX'
5739
- })
5827
+ @hd = HashDelegator.new(
5828
+ {
5829
+ block_calls_scan: 'CALLS_REGEX',
5830
+ block_name_wrapper_match: 'WRAPPER_REGEX'
5831
+ }
5832
+ )
5740
5833
  end
5741
5834
 
5742
5835
  def test_start_fenced_block
@@ -5754,9 +5847,7 @@ module MarkdownExec
5754
5847
 
5755
5848
  class TestHashDelegatorStringSendColor < Minitest::Test
5756
5849
  def setup
5757
- @hd = HashDelegator.new
5758
- @hd.instance_variable_set(:@delegate_object,
5759
- { red: 'red', green: 'green' })
5850
+ @hd = HashDelegator.new(red: 'red', green: 'green')
5760
5851
  end
5761
5852
 
5762
5853
  def test_string_send_color