markdown_exec 1.6 → 1.7

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.
@@ -0,0 +1,2709 @@
1
+ # encoding=utf-8
2
+ # frozen_string_literal: true
3
+
4
+ require 'English'
5
+ require 'clipboard'
6
+ require 'fileutils'
7
+ require 'open3'
8
+ require 'optparse'
9
+ require 'set'
10
+ require 'shellwords'
11
+ require 'tmpdir'
12
+ require 'tty-prompt'
13
+ require 'yaml'
14
+
15
+ require_relative 'array'
16
+ require_relative 'array_util'
17
+ require_relative 'block_label'
18
+ require_relative 'block_types'
19
+ require_relative 'cached_nested_file_reader'
20
+ require_relative 'constants'
21
+ require_relative 'exceptions'
22
+ require_relative 'fcb'
23
+ require_relative 'filter'
24
+ require_relative 'fout'
25
+ require_relative 'hash'
26
+ require_relative 'mdoc'
27
+ require_relative 'string_util'
28
+
29
+ class String
30
+ # Checks if the string is not empty.
31
+ # @return [Boolean] Returns true if the string is not empty, false otherwise.
32
+ def non_empty?
33
+ !empty?
34
+ end
35
+ end
36
+
37
+ module MarkdownExec
38
+ class DebugHelper
39
+ # Class-level variable to store history of printed messages
40
+ @@printed_messages = Set.new
41
+
42
+ # Outputs a warning message only once for a unique set of inputs
43
+ #
44
+ # @param str [Array] Variable number of arguments to be printed
45
+ def self.d(*str)
46
+ return if @@printed_messages.include?(str)
47
+
48
+ warn(*str)
49
+ @@printed_messages.add(str)
50
+ end
51
+ end
52
+
53
+ class HashDelegator
54
+ attr_accessor :run_state
55
+
56
+ def initialize(delegate_object = {})
57
+ @delegate_object = delegate_object
58
+ @prompt = tty_prompt_without_disabled_symbol
59
+
60
+ @run_state = OpenStruct.new(
61
+ link_history: []
62
+ )
63
+ @fout = FOut.new(@delegate_object) ### slice only relevant keys
64
+
65
+ @process_mutex = Mutex.new
66
+ @process_cv = ConditionVariable.new
67
+ end
68
+
69
+ # private
70
+
71
+ # def [](key)
72
+ # @delegate_object[key]
73
+ # end
74
+
75
+ # def []=(key, value)
76
+ # @delegate_object[key] = value
77
+ # end
78
+
79
+ # Modifies the provided menu blocks array by adding 'Back' and 'Exit' options,
80
+ # along with initial and final dividers, based on the delegate object's configuration.
81
+ #
82
+ # @param menu_blocks [Array] The array of menu block elements to be modified.
83
+ def add_menu_chrome_blocks!(menu_blocks)
84
+ return unless @delegate_object[:menu_link_format].present?
85
+
86
+ add_back_option(menu_blocks) if should_add_back_option?
87
+ add_exit_option(menu_blocks) if @delegate_object[:menu_with_exit]
88
+ add_dividers(menu_blocks)
89
+ end
90
+
91
+ private
92
+
93
+ def should_add_back_option?
94
+ @delegate_object[:menu_with_back] && history_env_state_exist?
95
+ end
96
+
97
+ def add_back_option(menu_blocks)
98
+ append_chrome_block(menu_blocks, MenuState::BACK)
99
+ end
100
+
101
+ def add_exit_option(menu_blocks)
102
+ append_chrome_block(menu_blocks, MenuState::EXIT)
103
+ end
104
+
105
+ def add_dividers(menu_blocks)
106
+ append_divider(menu_blocks, :initial)
107
+ append_divider(menu_blocks, :final)
108
+ end
109
+
110
+ public
111
+
112
+ # Appends a chrome block, which is a menu option for Back or Exit
113
+ #
114
+ # @param all_blocks [Array] The current blocks in the menu
115
+ # @param type [Symbol] The type of chrome block to add (:back or :exit)
116
+ def append_chrome_block(menu_blocks, type)
117
+ case type
118
+ when MenuState::BACK
119
+ state = history_state_partition
120
+ @hs_curr = state[:unit]
121
+ @hs_rest = state[:rest]
122
+ option_name = @delegate_object[:menu_option_back_name]
123
+ insert_at_top = @delegate_object[:menu_back_at_top]
124
+ when MenuState::EXIT
125
+ option_name = @delegate_object[:menu_option_exit_name]
126
+ insert_at_top = @delegate_object[:menu_exit_at_top]
127
+ end
128
+
129
+ formatted_name = format(@delegate_object[:menu_link_format],
130
+ safeval(option_name))
131
+ chrome_block = FCB.new(
132
+ chrome: true,
133
+
134
+ # dname: formatted_name.send(@delegate_object[:menu_link_color].to_sym),
135
+ dname: HashDelegator.new(@delegate_object).string_send_color(
136
+ formatted_name, :menu_link_color
137
+ ),
138
+ #dname: @delegate_object.string_send_color(formatted_name, :menu_link_color),
139
+
140
+ oname: formatted_name
141
+ )
142
+
143
+ if insert_at_top
144
+ menu_blocks.unshift(chrome_block)
145
+ else
146
+ menu_blocks.push(chrome_block)
147
+ end
148
+ end
149
+
150
+ # Appends a formatted divider to the specified position in a menu block array.
151
+ # The method checks for the presence of formatting options before appending.
152
+ #
153
+ # @param menu_blocks [Array] The array of menu block elements.
154
+ # @param position [Symbol] The position to insert the divider (:initial or :final).
155
+ def append_divider(menu_blocks, position)
156
+ return unless divider_formatting_present?(position)
157
+
158
+ divider = create_divider(position)
159
+ position == :initial ? menu_blocks.unshift(divider) : menu_blocks.push(divider)
160
+ end
161
+
162
+ # private
163
+
164
+ def divider_formatting_present?(position)
165
+ divider_key = position == :initial ? :menu_initial_divider : :menu_final_divider
166
+ @delegate_object[:menu_divider_format].present? && @delegate_object[divider_key].present?
167
+ end
168
+
169
+ def create_divider(position)
170
+ divider_key = position == :initial ? :menu_initial_divider : :menu_final_divider
171
+ oname = format(@delegate_object[:menu_divider_format],
172
+ safeval(@delegate_object[divider_key]))
173
+
174
+ FCB.new(
175
+ chrome: true,
176
+ disabled: '',
177
+ dname: string_send_color(oname, :menu_divider_color),
178
+ oname: oname
179
+ )
180
+ end
181
+
182
+ # Execute a code block after approval and provide user interaction options.
183
+ #
184
+ # This method displays required code blocks, asks for user approval, and
185
+ # executes the code block if approved. It also allows users to copy the
186
+ # code to the clipboard or save it to a file.
187
+ #
188
+ # @param opts [Hash] Options hash containing configuration settings.
189
+ # @param mdoc [YourMDocClass] An instance of the MDoc class.
190
+ #
191
+ def approve_and_execute_block(selected, mdoc)
192
+ if selected.fetch(:shell, '') == BlockType::LINK
193
+ handle_link_block(selected.fetch(:body, ''), mdoc, selected)
194
+ elsif @menu_user_clicked_back_link
195
+ handle_back_link
196
+ elsif selected[:shell] == BlockType::OPTS
197
+ handle_opts_block(selected, @menu_base_options)
198
+ else
199
+ handle_generic_block(mdoc, selected)
200
+ end
201
+ end
202
+
203
+ # private
204
+
205
+ # Searches for the first element in a collection where the specified key matches a given value.
206
+ # This method is particularly useful for finding a specific hash-like object within an enumerable collection.
207
+ # If no match is found, it returns a specified default value.
208
+ #
209
+ # @param blocks [Enumerable] The collection of hash-like objects to search.
210
+ # @param key [Object] The key to search for in each element of the collection.
211
+ # @param value [Object] The value to match against each element's corresponding key value.
212
+ # @param default [Object, nil] The default value to return if no match is found (optional).
213
+ # @return [Object, nil] The first matching element or the default value if no match is found.
214
+ def block_find(blocks, key, value, default = nil)
215
+ blocks.find { |item| item[key] == value } || default
216
+ end
217
+
218
+ # Iterates through nested files to collect various types of blocks, including dividers, tasks, and others.
219
+ # The method categorizes blocks based on their type and processes them accordingly.
220
+ #
221
+ # @return [Array<FCB>] An array of FCB objects representing the blocks.
222
+ def blocks_from_nested_files
223
+ blocks = []
224
+ iter_blocks_from_nested_files do |btype, fcb|
225
+ process_block_based_on_type(blocks, btype, fcb)
226
+ end
227
+ blocks
228
+ rescue StandardError
229
+ error_handler('blocks_from_nested_files')
230
+ end
231
+
232
+ # private
233
+
234
+ def process_block_based_on_type(blocks, btype, fcb)
235
+ case btype
236
+ when :blocks
237
+ blocks.push(get_block_summary(fcb))
238
+ when :filter
239
+ %i[blocks line]
240
+ when :line
241
+ unless @delegate_object[:no_chrome]
242
+ create_and_add_chrome_blocks(blocks,
243
+ fcb)
244
+ end
245
+ end
246
+ end
247
+
248
+ # private
249
+
250
+ def cfile
251
+ @cfile ||= CachedNestedFileReader.new(
252
+ import_pattern: @delegate_object.fetch(:import_pattern) #, "^ *@import +(?<name>.+?) *$")
253
+ )
254
+ end
255
+
256
+ # Check whether the document exists and is readable
257
+ def check_file_existence(filename)
258
+ unless filename&.present?
259
+ @fout.fout 'No blocks found.'
260
+ return false
261
+ end
262
+
263
+ unless File.exist? filename
264
+ @fout.fout 'Document is missing.'
265
+ return false
266
+ end
267
+ true
268
+ end
269
+
270
+ # Collects required code lines based on the selected block and the delegate object's configuration.
271
+ # If the block type is VARS, it also sets environment variables based on the block's content.
272
+ #
273
+ # @param mdoc [YourMDocClass] An instance of the MDoc class.
274
+ # @param selected [Hash] The selected block.
275
+ # @return [Array<String>] Required code blocks as an array of lines.
276
+ def collect_required_code_lines(mdoc, selected)
277
+ if selected[:shell] == BlockType::VARS
278
+ set_environment_variables(selected)
279
+ end
280
+
281
+ required = mdoc.collect_recursively_required_code(
282
+ @delegate_object[:block_name], opts: @delegate_object
283
+ )
284
+ read_required_blocks_from_temp_file + required[:code]
285
+ end
286
+
287
+ # private
288
+
289
+ def set_environment_variables(selected)
290
+ YAML.load(selected[:body].join("\n")).each do |key, value|
291
+ ENV[key] = value.to_s
292
+ next unless @delegate_object[:menu_vars_set_format].present?
293
+
294
+ formatted_string = format(@delegate_object[:menu_vars_set_format],
295
+ { key: key, value: value })
296
+ print string_send_color(formatted_string, :menu_vars_set_color)
297
+ end
298
+ end
299
+
300
+ def command_execute(command, args: [])
301
+ @run_state.files = Hash.new([])
302
+ @run_state.options = @delegate_object
303
+ @run_state.started_at = Time.now.utc
304
+
305
+ Open3.popen3(@delegate_object[:shell],
306
+ '-c', command,
307
+ @delegate_object[:filename],
308
+ *args) do |stdin, stdout, stderr, exec_thr|
309
+ handle_stream(stdout, ExecutionStreams::StdOut) do |line|
310
+ yield nil, line, nil, exec_thr if block_given?
311
+ end
312
+ handle_stream(stderr, ExecutionStreams::StdErr) do |line|
313
+ yield nil, nil, line, exec_thr if block_given?
314
+ end
315
+
316
+ in_thr = handle_stream($stdin, ExecutionStreams::StdIn) do |line|
317
+ stdin.puts(line)
318
+ yield line, nil, nil, exec_thr if block_given?
319
+ end
320
+
321
+ wait_for_stream_processing
322
+ exec_thr.join
323
+ sleep 0.1
324
+ in_thr.kill if in_thr&.alive?
325
+ end
326
+
327
+ @run_state.completed_at = Time.now.utc
328
+ rescue Errno::ENOENT => err
329
+ # Handle ENOENT error
330
+ @run_state.aborted_at = Time.now.utc
331
+ @run_state.error_message = err.message
332
+ @run_state.error = err
333
+ @run_state.files[ExecutionStreams::StdErr] += [@run_state.error_message]
334
+ @fout.fout "Error ENOENT: #{err.inspect}"
335
+ rescue SignalException => err
336
+ # Handle SignalException
337
+ @run_state.aborted_at = Time.now.utc
338
+ @run_state.error_message = 'SIGTERM'
339
+ @run_state.error = err
340
+ @run_state.files[ExecutionStreams::StdErr] += [@run_state.error_message]
341
+ @fout.fout "Error ENOENT: #{err.inspect}"
342
+ end
343
+
344
+ def command_or_user_selected_block(all_blocks, menu_blocks, default)
345
+ if @delegate_object[:block_name].present?
346
+ block = all_blocks.find do |item|
347
+ item[:oname] == @delegate_object[:block_name]
348
+ end
349
+ else
350
+ block_state = wait_for_user_selected_block(all_blocks, menu_blocks,
351
+ default)
352
+ block = block_state.block
353
+ state = block_state.state
354
+ end
355
+
356
+ SelectedBlockMenuState.new(block, state)
357
+ rescue StandardError
358
+ error_handler('command_or_user_selected_block')
359
+ end
360
+
361
+ def copy_to_clipboard(required_lines)
362
+ text = required_lines.flatten.join($INPUT_RECORD_SEPARATOR)
363
+ Clipboard.copy(text)
364
+ @fout.fout "Clipboard updated: #{required_lines.count} blocks," \
365
+ " #{required_lines.flatten.count} lines," \
366
+ " #{text.length} characters"
367
+ end
368
+
369
+ # Counts the number of fenced code blocks in a file.
370
+ # It reads lines from a file and counts occurrences of lines matching the fenced block regex.
371
+ # Assumes that every fenced block starts and ends with a distinct line (hence divided by 2).
372
+ #
373
+ # @return [Integer] The count of fenced code blocks in the file.
374
+ def count_blocks_in_filename
375
+ regex = Regexp.new(@delegate_object[:fenced_start_and_end_regex])
376
+ lines = cfile.readlines(@delegate_object[:filename])
377
+ count_matches_in_lines(lines, regex) / 2
378
+ end
379
+
380
+ # private
381
+
382
+ def count_matches_in_lines(lines, regex)
383
+ lines.count { |line| line.to_s.match(regex) }
384
+ end
385
+
386
+ # private
387
+
388
+ ##
389
+ # Creates and adds a formatted block to the blocks array based on the provided match and format options.
390
+ # @param blocks [Array] The array of blocks to add the new block to.
391
+ # @param match_data [MatchData] The match data containing named captures for formatting.
392
+ # @param format_option [String] The format string to be used for the new block.
393
+ # @param color_method [Symbol] The color method to apply to the block's display name.
394
+ def create_and_add_chrome_block(blocks, match_data, format_option,
395
+ color_method)
396
+ oname = format(format_option,
397
+ match_data.named_captures.transform_keys(&:to_sym))
398
+ blocks.push FCB.new(
399
+ chrome: true,
400
+ disabled: '',
401
+ dname: oname.send(color_method),
402
+ oname: oname
403
+ )
404
+ end
405
+
406
+ ##
407
+ # Processes lines within the file and converts them into blocks if they match certain criteria.
408
+ # @param blocks [Array] The array to append new blocks to.
409
+ # @param fcb [FCB] The file control block being processed.
410
+ # @param opts [Hash] Options containing configuration for line processing.
411
+ # @param use_chrome [Boolean] Indicates if the chrome styling should be applied.
412
+ def create_and_add_chrome_blocks(blocks, fcb)
413
+ match_criteria = [
414
+ { match: :menu_task_match, format: :menu_task_format,
415
+ color: :menu_task_color },
416
+ { match: :menu_divider_match, format: :menu_divider_format,
417
+ color: :menu_divider_color },
418
+ { match: :menu_note_match, format: :menu_note_format,
419
+ color: :menu_note_color }
420
+ ]
421
+ match_criteria.each do |criteria|
422
+ unless @delegate_object[criteria[:match]].present? &&
423
+ (mbody = fcb.body[0].match @delegate_object[criteria[:match]])
424
+ next
425
+ end
426
+
427
+ create_and_add_chrome_block(blocks, mbody, @delegate_object[criteria[:format]],
428
+ @delegate_object[criteria[:color]].to_sym)
429
+ break
430
+ end
431
+ end
432
+
433
+ # Creates a file at the specified path, writes the given content to it,
434
+ # and sets file permissions if required. Handles any errors encountered during the process.
435
+ #
436
+ # @param file_path [String] The path where the file will be created.
437
+ # @param content [String] The content to write into the file.
438
+ # @param chmod_value [Integer] The file permission value to set; skips if zero.
439
+ def create_and_write_file_with_permissions(file_path, content,
440
+ chmod_value)
441
+ create_directory_for_file(file_path)
442
+ write_file_content(file_path, content)
443
+ set_file_permissions(file_path, chmod_value) unless chmod_value.zero?
444
+ rescue StandardError
445
+ error_handler('create_and_write_file_with_permissions')
446
+ end
447
+
448
+ # private
449
+
450
+ def create_directory_for_file(file_path)
451
+ FileUtils.mkdir_p(File.dirname(file_path))
452
+ end
453
+
454
+ def write_file_content(file_path, content)
455
+ File.write(file_path, content)
456
+ end
457
+
458
+ def set_file_permissions(file_path, chmod_value)
459
+ File.chmod(chmod_value, file_path)
460
+ end
461
+
462
+ # Creates a temporary file, writes the provided code blocks into it,
463
+ # and sets an environment variable with the file path.
464
+ # @param code_blocks [String] Code blocks to write into the file.
465
+ def create_temp_file_with_code(code_blocks)
466
+ temp_file_path = create_temp_file
467
+ write_to_file(temp_file_path, code_blocks)
468
+ set_environment_variable(temp_file_path)
469
+ end
470
+
471
+ # private
472
+
473
+ def create_temp_file
474
+ Dir::Tmpname.create(self.class.to_s) { |path| path }
475
+ end
476
+
477
+ def write_to_file(path, content)
478
+ File.write(path, content)
479
+ end
480
+
481
+ def set_environment_variable(path)
482
+ ENV['MDE_LINK_REQUIRED_FILE'] = path
483
+ end
484
+
485
+ # Updates the title of an FCB object from its body content if the title is nil or empty.
486
+ def default_block_title_from_body(fcb)
487
+ return unless fcb.title.nil? || fcb.title.empty?
488
+
489
+ fcb.derive_title_from_body
490
+ end
491
+
492
+ def delete_blank_lines_next_to_chrome!(blocks_menu)
493
+ blocks_menu.process_and_conditionally_delete! do |prev_item, current_item, next_item|
494
+ (prev_item&.fetch(:chrome, nil) || next_item&.fetch(:chrome, nil)) &&
495
+ current_item&.fetch(:chrome, nil) &&
496
+ !current_item&.fetch(:oname).present?
497
+ end
498
+ end
499
+
500
+ # Deletes a temporary file specified by an environment variable.
501
+ # Checks if the file exists before attempting to delete it and clears the environment variable afterward.
502
+ # Any errors encountered during deletion are handled gracefully.
503
+ def delete_required_temp_file
504
+ temp_blocks_file_path = fetch_temp_blocks_file_path
505
+
506
+ return if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
507
+
508
+ safely_remove_file(temp_blocks_file_path)
509
+ clear_required_file
510
+ rescue StandardError
511
+ error_handler('delete_required_temp_file')
512
+ end
513
+
514
+ # private
515
+
516
+ def fetch_temp_blocks_file_path
517
+ ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
518
+ end
519
+
520
+ def safely_remove_file(path)
521
+ FileUtils.rm_f(path)
522
+ end
523
+
524
+ # Determines the state of a selected block in the menu based on the selected option.
525
+ # It categorizes the selected option into either EXIT, BACK, or CONTINUE state.
526
+ #
527
+ # @param selected_option [Hash] The selected menu option.
528
+ # @return [SelectedBlockMenuState] An object representing the state of the selected block.
529
+ def determine_block_state(selected_option)
530
+ option_name = selected_option.fetch(:oname, nil)
531
+ if option_name == menu_chrome_formatted_option(:menu_option_exit_name)
532
+ return SelectedBlockMenuState.new(nil,
533
+ MenuState::EXIT)
534
+ end
535
+ if option_name == menu_chrome_formatted_option(:menu_option_back_name)
536
+ return SelectedBlockMenuState.new(selected_option,
537
+ MenuState::BACK)
538
+ end
539
+
540
+ SelectedBlockMenuState.new(selected_option, MenuState::CONTINUE)
541
+ end
542
+
543
+ # Displays the required lines of code with color formatting for the preview section.
544
+ # It wraps the code lines between a formatted header and tail.
545
+ #
546
+ # @param required_lines [Array<String>] The lines of code to be displayed.
547
+ def display_required_code(required_lines)
548
+ output_color_formatted(:script_preview_head,
549
+ :script_preview_frame_color)
550
+ required_lines.each { |cb| @fout.fout cb }
551
+ output_color_formatted(:script_preview_tail,
552
+ :script_preview_frame_color)
553
+ end
554
+
555
+ # private
556
+
557
+ def output_color_formatted(data_sym, color_sym)
558
+ formatted_string = string_send_color(@delegate_object[data_sym],
559
+ color_sym)
560
+ @fout.fout formatted_string
561
+ end
562
+
563
+ def error_handler(name = '', opts = {})
564
+ Exceptions.error_handler(
565
+ "HashDelegator.#{name} -- #{$!}",
566
+ opts
567
+ )
568
+ end
569
+
570
+ # public
571
+
572
+ # Executes a block of code that has been approved for execution.
573
+ # It sets the script block name, writes command files if required, and handles the execution
574
+ # including output formatting and summarization.
575
+ #
576
+ # @param required_lines [Array<String>] The lines of code to be executed.
577
+ # @param selected [FCB] The selected functional code block object.
578
+ def execute_approved_block(required_lines = [], selected = FCB.new)
579
+ set_script_block_name(selected)
580
+ write_command_file_if_needed(required_lines)
581
+ format_and_execute_command(required_lines)
582
+ post_execution_process
583
+ end
584
+
585
+ # private
586
+
587
+ def set_script_block_name(selected)
588
+ @run_state.script_block_name = selected[:oname]
589
+ end
590
+
591
+ def write_command_file_if_needed(lines)
592
+ write_command_file(lines) if @delegate_object[:save_executed_script]
593
+ end
594
+
595
+ def format_and_execute_command(lines)
596
+ formatted_command = lines.flatten.join("\n")
597
+ @fout.fout fetch_color(data_sym: :script_execution_head,
598
+ color_sym: :script_execution_frame_color)
599
+ command_execute(formatted_command,
600
+ args: @delegate_object.fetch(:s_pass_args, []))
601
+ @fout.fout fetch_color(data_sym: :script_execution_tail,
602
+ color_sym: :script_execution_frame_color)
603
+ end
604
+
605
+ def post_execution_process
606
+ initialize_and_save_execution_output
607
+ output_execution_summary
608
+ output_execution_result
609
+ end
610
+
611
+ # Retrieves a specific data symbol from the delegate object, converts it to a string,
612
+ # and applies a color style based on the specified color symbol.
613
+ #
614
+ # @param default [String] The default value if the data symbol is not found.
615
+ # @param data_sym [Symbol] The symbol key to fetch data from the delegate object.
616
+ # @param color_sym [Symbol] The symbol key to fetch the color option for styling.
617
+ # @return [String] The color-styled string.
618
+ def fetch_color(default: '',
619
+ data_sym: :execution_report_preview_head,
620
+ color_sym: :execution_report_preview_frame_color)
621
+ data_string = @delegate_object.fetch(data_sym, default).to_s
622
+ string_send_color(data_string, color_sym)
623
+ end
624
+
625
+ # Formats a string based on a given context and applies color styling to it.
626
+ # It retrieves format and color information from the delegate object and processes accordingly.
627
+ #
628
+ # @param default [String] The default value if the format symbol is not found (unused in current implementation).
629
+ # @param context [Hash] Contextual data used for string formatting.
630
+ # @param format_sym [Symbol] Symbol key to fetch the format string from the delegate object.
631
+ # @param color_sym [Symbol] Symbol key to fetch the color option for string styling.
632
+ # @return [String] The formatted and color-styled string.
633
+ def format_references_send_color(default: '', context: {},
634
+ format_sym: :output_execution_label_format,
635
+ color_sym: :execution_report_preview_frame_color)
636
+ formatted_string = format(@delegate_object.fetch(format_sym, ''),
637
+ context).to_s
638
+ string_send_color(formatted_string, color_sym)
639
+ end
640
+
641
+ # Formats and returns the execution streams (like stdin, stdout, stderr) for a given key.
642
+ # It concatenates the array of strings found under the specified key in the run_state's files.
643
+ #
644
+ # @param key [Symbol] The key corresponding to the desired execution stream.
645
+ # @return [String] A concatenated string of the execution stream's contents.
646
+ def format_execution_streams(key)
647
+ files = @run_state.files || {}
648
+ files.fetch(key, []).join
649
+ end
650
+
651
+ # Processes a block to generate its summary, modifying its attributes based on various matching criteria.
652
+ # It handles special formatting for bash blocks, extracting and setting properties like call, stdin, stdout, and dname.
653
+ #
654
+ # @param fcb [Object] An object representing a functional code block.
655
+ # @return [Object] The modified functional code block with updated summary attributes.
656
+ def get_block_summary(fcb)
657
+ return fcb unless @delegate_object[:bash]
658
+
659
+ fcb.call = fcb.title.match(Regexp.new(@delegate_object[:block_calls_scan]))&.fetch(1, nil)
660
+ titlexcall = fcb.call ? fcb.title.sub("%#{fcb.call}", '') : fcb.title
661
+ bm = extract_named_captures_from_option(titlexcall,
662
+ @delegate_object[:block_name_match])
663
+
664
+ fcb.stdin = extract_named_captures_from_option(titlexcall,
665
+ @delegate_object[:block_stdin_scan])
666
+ fcb.stdout = extract_named_captures_from_option(titlexcall,
667
+ @delegate_object[:block_stdout_scan])
668
+
669
+ shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
670
+ fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall
671
+ fcb.dname = apply_shell_color_option(fcb.oname, shell_color_option)
672
+
673
+ fcb
674
+ end
675
+
676
+ # private
677
+
678
+ # Applies shell color options to the given string if applicable.
679
+ #
680
+ # @param name [String] The name to potentially colorize.
681
+ # @param shell_color_option [Symbol, nil] The shell color option to apply.
682
+ # @return [String] The colorized or original name string.
683
+ def apply_shell_color_option(name, shell_color_option)
684
+ if shell_color_option && @delegate_object[shell_color_option].present?
685
+ string_send_color(name, shell_color_option)
686
+ else
687
+ name
688
+ end
689
+ end
690
+
691
+ # This method handles the back-link operation in the Markdown execution context.
692
+ # It updates the history state and prepares to load the next block.
693
+ #
694
+ # @return [LoadFileNextBlock] An object indicating the action to load the next block.
695
+ def handle_back_link
696
+ history_state_pop
697
+ LoadFileNextBlock.new(LoadFile::Load, '')
698
+ end
699
+
700
+ # private
701
+ # Updates the delegate object's state based on the provided block state.
702
+ # It sets the block name and determines if the user clicked the back link in the menu.
703
+ #
704
+ # @param block_state [Object] An object representing the state of a block in the menu.
705
+ def handle_block_state(block_state)
706
+ unless [MenuState::BACK,
707
+ MenuState::CONTINUE].include?(block_state.state)
708
+ return
709
+ end
710
+
711
+ @delegate_object[:block_name] = block_state.block[:dname]
712
+ @menu_user_clicked_back_link = block_state.state == MenuState::BACK
713
+ end
714
+
715
+ # This method is responsible for handling the execution of generic blocks in a markdown document.
716
+ # It collects the required code lines from the document and, depending on the configuration,
717
+ # may display the code for user approval before execution. It then executes the approved block.
718
+ #
719
+ # @param mdoc [Object] The markdown document object containing code blocks.
720
+ # @param selected [Hash] The selected item from the menu to be executed.
721
+ # @return [LoadFileNextBlock] An object indicating whether to load the next block or reuse the current one.
722
+ def handle_generic_block(mdoc, selected)
723
+ required_lines = collect_required_code_lines(mdoc, selected)
724
+ output_or_approval = @delegate_object[:output_script] || @delegate_object[:user_must_approve]
725
+
726
+ display_required_code(required_lines) if output_or_approval
727
+
728
+ allow_execution = @delegate_object[:user_must_approve] ? prompt_for_user_approval(required_lines) : true
729
+
730
+ @delegate_object[:s_ir_approve] = allow_execution
731
+ execute_approved_block(required_lines, selected) if allow_execution
732
+
733
+ LoadFileNextBlock.new(LoadFile::Reuse, '')
734
+ end
735
+
736
+ # Handles the processing of a link block in Markdown Execution.
737
+ # It loads YAML data from the body content, pushes the state to history,
738
+ # sets environment variables, and decides on the next block to load.
739
+ #
740
+ # @param body [Array<String>] The body content as an array of strings.
741
+ # @param mdoc [Object] Markdown document object.
742
+ # @param selected [Boolean] Selected state.
743
+ # @return [LoadFileNextBlock] Object indicating the next action for file loading.
744
+ def handle_link_block(body, mdoc, selected)
745
+ data = parse_yaml_data_from_body(body)
746
+ data_file = data['file']
747
+ return LoadFileNextBlock.new(LoadFile::Reuse, '') unless data_file
748
+
749
+ history_state_push(mdoc, data_file, selected)
750
+ set_environment_variables(data['vars'])
751
+
752
+ LoadFileNextBlock.new(LoadFile::Load, data['block'] || '')
753
+ end
754
+
755
+ # private
756
+
757
+ def parse_yaml_data_from_body(body)
758
+ body.any? ? YAML.load(body.join("\n")) : {}
759
+ end
760
+
761
+ def set_environment_variables(vars)
762
+ vars ||= []
763
+ vars.each { |key, value| ENV[key] = value.to_s }
764
+ end
765
+
766
+ # Processes YAML data from the selected menu item, updating delegate objects and optionally printing formatted output.
767
+ # @param selected [Hash] Selected item from the menu containing a YAML body.
768
+ # @param tgt2 [Hash, nil] An optional target hash to update with YAML data.
769
+ # @return [LoadFileNextBlock] An instance indicating the next action for loading files.
770
+ def handle_opts_block(selected, tgt2 = nil)
771
+ data = YAML.load(selected[:body].join("\n"))
772
+ data.each do |key, value|
773
+ update_delegate_and_target(key, value, tgt2)
774
+ if @delegate_object[:menu_opts_set_format].present?
775
+ print_formatted_option(key,
776
+ value)
777
+ end
778
+ end
779
+ LoadFileNextBlock.new(LoadFile::Reuse, '')
780
+ end
781
+
782
+ # private
783
+
784
+ def update_delegate_and_target(key, value, tgt2)
785
+ sym_key = key.to_sym
786
+ @delegate_object[sym_key] = value
787
+ tgt2[sym_key] = value if tgt2
788
+ end
789
+
790
+ def print_formatted_option(key, value)
791
+ formatted_str = format(@delegate_object[:menu_opts_set_format],
792
+ { key: key, value: value })
793
+ print string_send_color(formatted_str, :menu_opts_set_color)
794
+ end
795
+
796
+ def handle_stream(stream, file_type, swap: false)
797
+ @process_mutex.synchronize do
798
+ Thread.new do
799
+ stream.each_line do |line|
800
+ line.strip!
801
+ @run_state.files[file_type] << line
802
+
803
+ if @delegate_object[:output_stdout]
804
+ # print line
805
+ puts line
806
+ end
807
+
808
+ yield line if block_given?
809
+ end
810
+ rescue IOError
811
+ # Handle IOError
812
+ ensure
813
+ @process_cv.signal
814
+ end
815
+ end
816
+ end
817
+
818
+ def wait_for_stream_processing
819
+ @process_mutex.synchronize do
820
+ @process_cv.wait(@process_mutex)
821
+ end
822
+ end
823
+
824
+ # Partitions the history state from the environment variable based on the document separator.
825
+ # @return [Hash] A hash containing two parts: :unit (first part) and :rest (remaining part).
826
+ def history_state_partition
827
+ history_env_value = ENV.fetch(MDE_HISTORY_ENV_NAME, '')
828
+ separator = @delegate_object[:history_document_separator]
829
+
830
+ unit, rest = StringUtil.partition_at_first(history_env_value, separator)
831
+ { unit: unit, rest: rest }
832
+ end
833
+
834
+ # Pops the last entry from the history state, updating the delegate object and environment variable.
835
+ # It also deletes the required temporary file and updates the run state link history.
836
+ def history_state_pop
837
+ state = history_state_partition
838
+ @delegate_object[:filename] = state[:unit]
839
+ ENV[MDE_HISTORY_ENV_NAME] = state[:rest]
840
+ delete_required_temp_file
841
+ @run_state.link_history.pop
842
+ end
843
+
844
+ # Updates the history state by pushing a new entry and managing environment variables.
845
+ # @param mdoc [Object] The Markdown document object.
846
+ # @param data_file [String] The data file to be processed.
847
+ # @param selected [Hash] Hash containing the selected block's name.
848
+ def history_state_push(mdoc, data_file, selected)
849
+ # Construct new history string
850
+ new_history = [@delegate_object[:filename],
851
+ @delegate_object[:history_document_separator],
852
+ ENV.fetch(MDE_HISTORY_ENV_NAME, '')].join
853
+
854
+ # Update delegate object and environment variable
855
+ @delegate_object[:filename] = data_file
856
+ ENV[MDE_HISTORY_ENV_NAME] = new_history
857
+
858
+ # Write required blocks to temp file and update run state
859
+ write_required_blocks_to_temp_file(mdoc, @delegate_object[:block_name])
860
+ @run_state.link_history.push(block_name: selected[:oname],
861
+ filename: data_file)
862
+ end
863
+
864
+ # Indents all lines in a given string with a specified indentation string.
865
+ # @param body [String] A multi-line string to be indented.
866
+ # @param indent [String] The string used for indentation (default is an empty string).
867
+ # @return [String] A single string with each line indented as specified.
868
+ def indent_all_lines(body, indent = nil)
869
+ return body unless indent&.non_empty?
870
+
871
+ body.lines.map { |line| indent + line.chomp }.join("\n")
872
+ end
873
+
874
+ def initialize_fcb_names(fcb)
875
+ fcb.oname = fcb.dname = fcb.title || ''
876
+ end
877
+
878
+ # Initializes variables for regex and other states
879
+ def initial_state
880
+ {
881
+ fenced_start_and_end_regex: Regexp.new(@delegate_object.fetch(
882
+ :fenced_start_and_end_regex, '^(?<indent> *)`{3,}'
883
+ )),
884
+ fenced_start_extended_regex: Regexp.new(@delegate_object.fetch(
885
+ :fenced_start_and_end_regex, '^(?<indent> *)`{3,}'
886
+ )),
887
+ fcb: MarkdownExec::FCB.new,
888
+ in_fenced_block: false,
889
+ headings: []
890
+ }
891
+ end
892
+
893
+ def initialize_and_save_execution_output
894
+ return unless @delegate_object[:save_execution_output]
895
+
896
+ @delegate_object[:logged_stdout_filename] =
897
+ SavedAsset.stdout_name(blockname: @delegate_object[:block_name],
898
+ filename: File.basename(@delegate_object[:filename],
899
+ '.*'),
900
+ prefix: @delegate_object[:logged_stdout_filename_prefix],
901
+ time: Time.now.utc)
902
+
903
+ @logged_stdout_filespec =
904
+ @delegate_object[:logged_stdout_filespec] =
905
+ File.join @delegate_object[:saved_stdout_folder],
906
+ @delegate_object[:logged_stdout_filename]
907
+ @logged_stdout_filespec = @delegate_object[:logged_stdout_filespec]
908
+ write_execution_output_to_file
909
+ end
910
+
911
+ # Iterates through blocks in a file, applying the provided block to each line.
912
+ # The iteration only occurs if the file exists.
913
+ # @yield [Symbol] :filter Yields to obtain selected messages for processing.
914
+ def iter_blocks_from_nested_files(&block)
915
+ return unless check_file_existence(@delegate_object[:filename])
916
+
917
+ state = initial_state
918
+ selected_messages = yield :filter
919
+
920
+ cfile.readlines(@delegate_object[:filename]).each do |nested_line|
921
+ if nested_line
922
+ update_line_and_block_state(nested_line, state, selected_messages,
923
+ &block)
924
+ end
925
+ end
926
+ end
927
+
928
+ # Loads auto blocks based on delegate object settings and updates if new filename is detected.
929
+ # Executes a specified block once per filename.
930
+ # @param all_blocks [Array] Array of all block elements.
931
+ # @return [Boolean, nil] True if values were modified, nil otherwise.
932
+ def load_auto_blocks(all_blocks)
933
+ block_name = @delegate_object[:document_load_opts_block_name]
934
+ unless block_name.present? && @delegate_object[:s_most_recent_filename] != @delegate_object[:filename]
935
+ return
936
+ end
937
+
938
+ block = block_find(all_blocks, :oname, block_name)
939
+ return unless block
940
+
941
+ handle_opts_block(block, @delegate_object)
942
+ @delegate_object[:s_most_recent_filename] = @delegate_object[:filename]
943
+ true
944
+ end
945
+
946
+ # DebugHelper.d ["HDmm method_name: #{method_name}", "#{first_n_caller_items 1}"]
947
+ def first_n_caller_items(n)
948
+ # Get the call stack
949
+ call_stack = caller
950
+ base_path = File.realpath('.')
951
+
952
+ # Modify the call stack to remove the base path and keep only the first n items
953
+ call_stack.take(n + 1)[1..].map do |line|
954
+ " . #{line.sub(/^#{Regexp.escape(base_path)}\//, '')}"
955
+ end.join("\n")
956
+ end
957
+
958
+ # Checks if a history environment variable is set and returns its value if present.
959
+ # @return [String, nil] The value of the history environment variable or nil if not present.
960
+ def history_env_state_exist?
961
+ ENV.fetch(MDE_HISTORY_ENV_NAME, '').present?
962
+ end
963
+
964
+ # def history_env_state_exist?
965
+ # history = ENV.fetch(MDE_HISTORY_ENV_NAME, '')
966
+ # history.present? ? history : nil
967
+ # end
968
+
969
+ # If a method is missing, treat it as a key for the @delegate_object.
970
+ def method_missing(method_name, *args, &block)
971
+ if @delegate_object.respond_to?(method_name)
972
+ @delegate_object.send(method_name, *args, &block)
973
+ elsif method_name.to_s.end_with?('=') && args.size == 1
974
+ @delegate_object[method_name.to_s.chop.to_sym] = args.first
975
+ else
976
+ @delegate_object[method_name]
977
+ # super
978
+ end
979
+ end
980
+
981
+ def mdoc_and_blocks_from_nested_files
982
+ menu_blocks = blocks_from_nested_files
983
+ mdoc = MDoc.new(menu_blocks) do |nopts|
984
+ @delegate_object.merge!(nopts)
985
+ end
986
+ [menu_blocks, mdoc]
987
+ end
988
+
989
+ ## Handles the file loading and returns the blocks in the file and MDoc instance
990
+ #
991
+ def mdoc_menu_and_blocks_from_nested_files
992
+ all_blocks, mdoc = mdoc_and_blocks_from_nested_files
993
+
994
+ # recreate menu with new options
995
+ #
996
+ if load_auto_blocks(all_blocks)
997
+ all_blocks, mdoc = mdoc_and_blocks_from_nested_files
998
+ end
999
+
1000
+ menu_blocks = mdoc.fcbs_per_options(@delegate_object)
1001
+ add_menu_chrome_blocks!(menu_blocks)
1002
+ delete_blank_lines_next_to_chrome!(menu_blocks)
1003
+ [all_blocks, menu_blocks, mdoc]
1004
+ end
1005
+
1006
+ # Formats and optionally colors a menu option based on delegate object's configuration.
1007
+ # @param option_symbol [Symbol] The symbol key for the menu option in the delegate object.
1008
+ # @return [String] The formatted and possibly colored value of the menu option.
1009
+ def menu_chrome_colored_option(option_symbol = :menu_option_back_name)
1010
+ formatted_option = menu_chrome_formatted_option(option_symbol)
1011
+ return formatted_option unless @delegate_object[:menu_chrome_color]
1012
+
1013
+ string_send_color(formatted_option, :menu_chrome_color)
1014
+ end
1015
+
1016
+ # Formats a menu option based on the delegate object's configuration.
1017
+ # It safely evaluates the value of the option and optionally formats it.
1018
+ # @param option_symbol [Symbol] The symbol key for the menu option in the delegate object.
1019
+ # @return [String] The formatted or original value of the menu option.
1020
+ def menu_chrome_formatted_option(option_symbol = :menu_option_back_name)
1021
+ option_value = safeval(@delegate_object.fetch(option_symbol, ''))
1022
+
1023
+ if @delegate_object[:menu_chrome_format]
1024
+ format(@delegate_object[:menu_chrome_format], option_value)
1025
+ else
1026
+ option_value
1027
+ end
1028
+ end
1029
+
1030
+ def next_block_name_from_command_line_arguments
1031
+ return MenuControl::Repeat unless @delegate_object[:s_cli_rest].present?
1032
+
1033
+ @delegate_object[:block_name] = @delegate_object[:s_cli_rest].pop
1034
+ MenuControl::Fresh
1035
+ end
1036
+
1037
+ def output_execution_result
1038
+ @fout.fout fetch_color(data_sym: :execution_report_preview_head,
1039
+ color_sym: :execution_report_preview_frame_color)
1040
+ [
1041
+ ['Block', @run_state.script_block_name],
1042
+ ['Command', ([MarkdownExec::BIN_NAME, @delegate_object[:filename]] +
1043
+ (@run_state.link_history.map { |item|
1044
+ item[:block_name]
1045
+ }) +
1046
+ [@run_state.script_block_name]).join(' ')],
1047
+ ['Script', @run_state.saved_filespec],
1048
+ ['StdOut', @delegate_object[:logged_stdout_filespec]]
1049
+ ].each do |label, value|
1050
+ next unless value
1051
+
1052
+ output_labeled_value(label, value, DISPLAY_LEVEL_ADMIN)
1053
+ end
1054
+ @fout.fout fetch_color(data_sym: :execution_report_preview_tail,
1055
+ color_sym: :execution_report_preview_frame_color)
1056
+ end
1057
+
1058
+ def output_execution_summary
1059
+ return unless @delegate_object[:output_execution_summary]
1060
+
1061
+ fout_section 'summary', {
1062
+ execute_aborted_at: @run_state.aborted_at,
1063
+ execute_completed_at: @run_state.completed_at,
1064
+ execute_error: @run_state.error,
1065
+ execute_error_message: @run_state.error_message,
1066
+ execute_files: @run_state.files,
1067
+ execute_options: @run_state.options,
1068
+ execute_started_at: @run_state.started_at,
1069
+ script_block_name: @run_state.script_block_name,
1070
+ saved_filespec: @run_state.saved_filespec
1071
+ }
1072
+ end
1073
+
1074
+ def output_labeled_value(label, value, level)
1075
+ @fout.lout format_references_send_color(
1076
+ context: { name: string_send_color(label, :output_execution_label_name_color),
1077
+ value: string_send_color(value.to_s,
1078
+ :output_execution_label_value_color) },
1079
+ format_sym: :output_execution_label_format
1080
+ ), level: level
1081
+ end
1082
+
1083
+ # Prepare the blocks menu by adding labels and other necessary details.
1084
+ #
1085
+ # @param all_blocks [Array<Hash>] The list of blocks from the file.
1086
+ # @param opts [Hash] The options hash.
1087
+ # @return [Array<Hash>] The updated blocks menu.
1088
+ def prepare_blocks_menu(menu_blocks)
1089
+ ### replace_consecutive_blanks(menu_blocks).map do |fcb|
1090
+ menu_blocks.map do |fcb|
1091
+ next if Filter.prepared_not_in_menu?(@delegate_object, fcb,
1092
+ %i[block_name_include_match block_name_wrapper_match])
1093
+
1094
+ fcb.merge!(
1095
+ name: indent_all_lines(fcb.dname, fcb.fetch(:indent, nil)),
1096
+ label: BlockLabel.make(
1097
+ body: fcb[:body],
1098
+ filename: @delegate_object[:filename],
1099
+ headings: fcb.fetch(:headings, []),
1100
+ menu_blocks_with_docname: @delegate_object[:menu_blocks_with_docname],
1101
+ menu_blocks_with_headings: @delegate_object[:menu_blocks_with_headings],
1102
+ text: fcb[:text],
1103
+ title: fcb[:title]
1104
+ )
1105
+ )
1106
+ fcb.to_h
1107
+ end.compact
1108
+ end
1109
+
1110
+ ##
1111
+ # Presents a menu to the user for approving an action and performs additional tasks based on the selection.
1112
+ # The function provides options for approval, rejection, copying data to clipboard, or saving data to a file.
1113
+ #
1114
+ # @param opts [Hash] A hash containing various options for the menu.
1115
+ # @param required_lines [Array<String>] Lines of text or code that are subject to user approval.
1116
+ #
1117
+ # @option opts [String] :prompt_approve_block Prompt text for the approval menu.
1118
+ # @option opts [String] :prompt_yes Text for the 'Yes' choice in the menu.
1119
+ # @option opts [String] :prompt_no Text for the 'No' choice in the menu.
1120
+ # @option opts [String] :prompt_script_to_clipboard Text for the 'Copy to Clipboard' choice in the menu.
1121
+ # @option opts [String] :prompt_save_script Text for the 'Save to File' choice in the menu.
1122
+ #
1123
+ # @return [Boolean] Returns true if the user approves (selects 'Yes'), false otherwise.
1124
+ ##
1125
+ def prompt_for_user_approval(required_lines)
1126
+ # Present a selection menu for user approval.
1127
+ sel = @prompt.select(
1128
+ string_send_color(@delegate_object[:prompt_approve_block],
1129
+ :prompt_color_after_script_execution),
1130
+ filter: true
1131
+ ) do |menu|
1132
+ # sel = @prompt.select(@delegate_object[:prompt_approve_block], filter: true) do |menu|
1133
+ menu.default MenuOptions::YES
1134
+ menu.choice @delegate_object[:prompt_yes], MenuOptions::YES
1135
+ menu.choice @delegate_object[:prompt_no], MenuOptions::NO
1136
+ menu.choice @delegate_object[:prompt_script_to_clipboard],
1137
+ MenuOptions::SCRIPT_TO_CLIPBOARD
1138
+ menu.choice @delegate_object[:prompt_save_script],
1139
+ MenuOptions::SAVE_SCRIPT
1140
+ end
1141
+
1142
+ if sel == MenuOptions::SCRIPT_TO_CLIPBOARD
1143
+ copy_to_clipboard(required_lines)
1144
+ elsif sel == MenuOptions::SAVE_SCRIPT
1145
+ save_to_file(required_lines)
1146
+ end
1147
+
1148
+ sel == MenuOptions::YES
1149
+ rescue TTY::Reader::InputInterrupt
1150
+ exit 1
1151
+ end
1152
+
1153
+ def prompt_select_continue
1154
+ sel = @prompt.select(
1155
+ string_send_color(@delegate_object[:prompt_after_script_execution],
1156
+ :prompt_color_after_script_execution),
1157
+ filter: true,
1158
+ quiet: true
1159
+ ) do |menu|
1160
+ menu.choice @delegate_object[:prompt_yes]
1161
+ menu.choice @delegate_object[:prompt_exit]
1162
+ end
1163
+ sel == @delegate_object[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
1164
+ rescue TTY::Reader::InputInterrupt
1165
+ exit 1
1166
+ end
1167
+
1168
+ def save_to_file(required_lines)
1169
+ write_command_file(required_lines)
1170
+ @fout.fout "File saved: #{@run_state.saved_filespec}"
1171
+ end
1172
+
1173
+ # public
1174
+
1175
+ # Reads required code blocks from a temporary file specified by an environment variable.
1176
+ # @return [Array<String>] Lines read from the temporary file, or an empty array if file is not found or path is empty.
1177
+ def read_required_blocks_from_temp_file
1178
+ temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
1179
+ return [] if temp_blocks_file_path.to_s.empty?
1180
+
1181
+ if File.exist?(temp_blocks_file_path)
1182
+ File.readlines(
1183
+ temp_blocks_file_path, chomp: true
1184
+ )
1185
+ else
1186
+ []
1187
+ end
1188
+ end
1189
+
1190
+ # Evaluates the given string as Ruby code and rescues any StandardErrors.
1191
+ # If an error occurs, it calls the error_handler method with 'safeval'.
1192
+ # @param str [String] The string to be evaluated.
1193
+ # @return [Object] The result of evaluating the string.
1194
+ def safeval(str)
1195
+ eval(str)
1196
+ rescue StandardError
1197
+ error_handler('safeval')
1198
+ end
1199
+
1200
+ # def safeval(str)
1201
+ # eval(str)
1202
+ # rescue StandardError
1203
+ # error_handler('safeval')
1204
+ # end
1205
+
1206
+ # Select and execute a code block from a Markdown document.
1207
+ #
1208
+ # This method allows the user to interactively select a code block from a
1209
+ # Markdown document, obtain approval, and execute the chosen block of code.
1210
+ #
1211
+ # @return [Nil] Returns nil if no code block is selected or an error occurs.
1212
+ def select_approve_and_execute_block
1213
+ @menu_base_options = @delegate_object
1214
+ repeat_menu = @menu_base_options[:block_name].present? ? MenuControl::Fresh : MenuControl::Repeat
1215
+ load_file_next_block = LoadFileNextBlock.new(LoadFile::Reuse)
1216
+ default = nil
1217
+
1218
+ @menu_state_filename = @menu_base_options[:filename]
1219
+ @menu_state_block_name = @menu_base_options[:block_name]
1220
+
1221
+ loop do
1222
+ loop do
1223
+ @delegate_object = @menu_base_options.dup
1224
+ @menu_base_options[:filename] = @menu_state_filename
1225
+ @menu_base_options[:block_name] = @menu_state_block_name
1226
+ @menu_state_filename = nil
1227
+ @menu_state_block_name = nil
1228
+
1229
+ @menu_user_clicked_back_link = false
1230
+ blocks_in_file, menu_blocks, mdoc = mdoc_menu_and_blocks_from_nested_files
1231
+ block_state = command_or_user_selected_block(blocks_in_file,
1232
+ menu_blocks, default)
1233
+ return if block_state.state == MenuState::EXIT
1234
+
1235
+ if block_state.block.nil?
1236
+ warn_format('select_approve_and_execute_block', "Block not found -- #{@delegate_object[:block_name]}",
1237
+ { abort: true })
1238
+ # error_handler("Block not found -- #{opts[:block_name]}", { abort: true })
1239
+ end
1240
+
1241
+ load_file_next_block = approve_and_execute_block(block_state.block,
1242
+ mdoc)
1243
+ default = load_file_next_block.load_file == LoadFile::Load ? nil : @delegate_object[:block_name]
1244
+ @menu_base_options[:block_name] =
1245
+ @delegate_object[:block_name] = load_file_next_block.next_block
1246
+ @menu_base_options[:filename] = @delegate_object[:filename]
1247
+
1248
+ # user prompt to exit if the menu will be displayed again
1249
+ #
1250
+ if repeat_menu == MenuControl::Repeat &&
1251
+ block_state.block[:shell] == BlockType::BASH &&
1252
+ @delegate_object[:pause_after_script_execution] &&
1253
+ prompt_select_continue == MenuState::EXIT
1254
+ return
1255
+ end
1256
+
1257
+ # exit current document/menu if loading next document or single block_name was specified
1258
+ #
1259
+ if block_state.state == MenuState::CONTINUE && load_file_next_block.load_file == LoadFile::Load
1260
+ break
1261
+ end
1262
+ break if repeat_menu == MenuControl::Fresh
1263
+ end
1264
+ break if load_file_next_block.load_file == LoadFile::Reuse
1265
+
1266
+ repeat_menu = next_block_name_from_command_line_arguments
1267
+ @menu_state_filename = @menu_base_options[:filename]
1268
+ @menu_state_block_name = @menu_base_options[:block_name]
1269
+ end
1270
+ rescue StandardError
1271
+ error_handler('select_approve_and_execute_block',
1272
+ { abort: true })
1273
+ end
1274
+
1275
+ # Presents a TTY prompt to select an option or exit, returns metadata including option and selected
1276
+ def select_option_with_metadata(prompt_text, names, opts = {})
1277
+ selection = @prompt.select(prompt_text,
1278
+ names,
1279
+ opts.merge(filter: true))
1280
+ item = if names.first.instance_of?(String)
1281
+ { dname: selection }
1282
+ else
1283
+ names.find { |item| item[:dname] == selection }
1284
+ end
1285
+
1286
+ item.merge(
1287
+ if selection == menu_chrome_colored_option(:menu_option_back_name)
1288
+ { option: selection, curr: @hs_curr, rest: @hs_rest,
1289
+ shell: BlockType::LINK }
1290
+ elsif selection == menu_chrome_colored_option(:menu_option_exit_name)
1291
+ { option: selection }
1292
+ else
1293
+ { selected: selection }
1294
+ end
1295
+ )
1296
+ rescue TTY::Reader::InputInterrupt
1297
+ exit 1
1298
+ rescue StandardError
1299
+ error_handler('select_option_with_metadata')
1300
+ end
1301
+
1302
+ # Initializes a new fenced code block (FCB) object based on the provided line and heading information.
1303
+ # @param line [String] The line initiating the fenced block.
1304
+ # @param headings [Array<String>] Current headings hierarchy.
1305
+ # @param fenced_start_extended_regex [Regexp] Regular expression to identify fenced block start.
1306
+ # @return [MarkdownExec::FCB] A new FCB instance with the parsed attributes.
1307
+ def start_fenced_block(line, headings, fenced_start_extended_regex)
1308
+ fcb_title_groups = line.match(fenced_start_extended_regex).named_captures.sym_keys
1309
+ rest = fcb_title_groups.fetch(:rest, '')
1310
+ reqs, wraps =
1311
+ ArrayUtil.partition_by_predicate(rest.scan(/\+[^\s]+/).map do |req|
1312
+ req[1..-1]
1313
+ end) do |name|
1314
+ !name.match(Regexp.new(@delegate_object[:block_name_wrapper_match]))
1315
+ end
1316
+
1317
+ MarkdownExec::FCB.new(
1318
+ body: [],
1319
+ call: rest.match(Regexp.new(@delegate_object[:block_calls_scan]))&.to_a&.first,
1320
+ dname: fcb_title_groups.fetch(:name, ''),
1321
+ headings: headings,
1322
+ indent: fcb_title_groups.fetch(:indent, ''),
1323
+ oname: fcb_title_groups.fetch(:name, ''),
1324
+ reqs: reqs,
1325
+ shell: fcb_title_groups.fetch(:shell, ''),
1326
+ stdin: if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
1327
+ tn.named_captures.sym_keys
1328
+ end,
1329
+ stdout: if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
1330
+ tn.named_captures.sym_keys
1331
+ end,
1332
+ title: fcb_title_groups.fetch(:name, ''),
1333
+ wraps: wraps
1334
+ )
1335
+ end
1336
+
1337
+ # Applies a color method to a string based on the provided color symbol.
1338
+ # The color method is fetched from @delegate_object and applied to the string.
1339
+ # @param string [String] The string to which the color will be applied.
1340
+ # @param color_sym [Symbol] The symbol representing the color method.
1341
+ # @param default [String] Default color method to use if color_sym is not found in @delegate_object.
1342
+ # @return [String] The string with the applied color method.
1343
+ def string_send_color(string, color_sym, default: 'plain')
1344
+ color_method = @delegate_object.fetch(color_sym, default).to_sym
1345
+ string.to_s.send(color_method)
1346
+ end
1347
+
1348
+ # Creates a TTY prompt with custom settings. Specifically, it disables the default 'cross' symbol and
1349
+ # defines a lambda function to handle interrupts.
1350
+ # @return [TTY::Prompt] A new TTY::Prompt instance with specified configurations.
1351
+ def tty_prompt_without_disabled_symbol
1352
+ TTY::Prompt.new(
1353
+ interrupt: lambda {
1354
+ puts
1355
+ raise TTY::Reader::InputInterrupt
1356
+ },
1357
+ symbols: { cross: ' ' }
1358
+ )
1359
+ end
1360
+
1361
+ # Updates the hierarchy of document headings based on the given line.
1362
+ # Utilizes regular expressions to identify heading levels.
1363
+ # @param line [String] The line of text to check for headings.
1364
+ # @param headings [Array<String>] Current headings hierarchy.
1365
+ # @return [Array<String>] Updated headings hierarchy.
1366
+ def update_document_headings(line, headings)
1367
+ heading3_match = Regexp.new(@delegate_object[:heading3_match])
1368
+ heading2_match = Regexp.new(@delegate_object[:heading2_match])
1369
+ heading1_match = Regexp.new(@delegate_object[:heading1_match])
1370
+
1371
+ case line
1372
+ when heading3_match
1373
+ [headings[0], headings[1], $~[:name]]
1374
+ when heading2_match
1375
+ [headings[0], $~[:name]]
1376
+ when heading1_match
1377
+ [$~[:name]]
1378
+ else
1379
+ headings
1380
+ end
1381
+ end
1382
+
1383
+ ##
1384
+ # Processes an individual line within a loop, updating headings and handling fenced code blocks.
1385
+ # This function is designed to be called within a loop that iterates through each line of a document.
1386
+ #
1387
+ # @param line [String] The current line being processed.
1388
+ # @param state [Hash] The current state of the parser, including flags and data related to the processing.
1389
+ # @param opts [Hash] A hash containing various options for line and block processing.
1390
+ # @param selected_messages [Array<String>] Accumulator for lines or messages that are subject to further processing.
1391
+ # @param block [Proc] An optional block for further processing or transformation of lines.
1392
+ #
1393
+ # @option state [Array<String>] :headings Current headings to be updated based on the line.
1394
+ # @option state [Regexp] :fenced_start_and_end_regex Regular expression to match the start and end of a fenced block.
1395
+ # @option state [Boolean] :in_fenced_block Flag indicating whether the current line is inside a fenced block.
1396
+ # @option state [Object] :fcb An object representing the current fenced code block being processed.
1397
+ #
1398
+ # @option opts [Boolean] :menu_blocks_with_headings Flag indicating whether to update headings while processing.
1399
+ #
1400
+ # @return [Void] The function modifies the `state` and `selected_messages` arguments in place.
1401
+ ##
1402
+ def update_line_and_block_state(nested_line, state, selected_messages,
1403
+ &block)
1404
+ line = nested_line.to_s
1405
+ if @delegate_object[:menu_blocks_with_headings]
1406
+ state[:headings] = update_document_headings(line, state[:headings])
1407
+ end
1408
+
1409
+ if line.match(@delegate_object[:fenced_start_and_end_regex])
1410
+ if state[:in_fenced_block]
1411
+ ## end of code block
1412
+ #
1413
+ update_menu_attrib_yield_selected(state[:fcb], selected_messages,
1414
+ &block)
1415
+ state[:in_fenced_block] = false
1416
+ else
1417
+ ## start of code block
1418
+ #
1419
+ state[:fcb] =
1420
+ start_fenced_block(line, state[:headings],
1421
+ @delegate_object[:fenced_start_extended_regex])
1422
+ state[:fcb][:depth] = nested_line[:depth]
1423
+ state[:in_fenced_block] = true
1424
+ end
1425
+ elsif state[:in_fenced_block] && state[:fcb].body
1426
+ ## add line to fenced code block
1427
+ # remove fcb indent if possible
1428
+ #
1429
+ # if nested_line[:depth].zero? || opts[:menu_include_imported_blocks]
1430
+ # if add_import_block?(nested_line)
1431
+ state[:fcb].body += [
1432
+ line.chomp.sub(/^#{state[:fcb].indent}/, '')
1433
+ ]
1434
+ # end
1435
+ elsif nested_line[:depth].zero? || @delegate_object[:menu_include_imported_notes]
1436
+ # add line if it is depth 0 or option allows it
1437
+ #
1438
+ yield_line_if_selected(line, selected_messages, &block)
1439
+ end
1440
+ end
1441
+
1442
+ # Updates the attributes of the given fcb object and conditionally yields to a block.
1443
+ # It initializes fcb names and sets the default block title from fcb's body.
1444
+ # If the fcb has a body and meets certain conditions, it yields to the given block.
1445
+ #
1446
+ # @param fcb [Object] The fcb object whose attributes are to be updated.
1447
+ # @param selected_messages [Array<Symbol>] A list of message types to determine if yielding is applicable.
1448
+ # @param block [Block] An optional block to yield to if conditions are met.
1449
+ def update_menu_attrib_yield_selected(fcb, selected_messages, &block)
1450
+ initialize_fcb_names(fcb)
1451
+ return unless fcb.body
1452
+
1453
+ default_block_title_from_body(fcb)
1454
+ yield_to_block_if_applicable(fcb, selected_messages, &block)
1455
+ end
1456
+
1457
+ def wait_for_user_selected_block(all_blocks, menu_blocks, default)
1458
+ block_state = wait_for_user_selection(all_blocks, menu_blocks, default)
1459
+ handle_block_state(block_state)
1460
+
1461
+ block_state
1462
+ rescue StandardError
1463
+ error_handler('wait_for_user_selected_block')
1464
+ end
1465
+
1466
+ def wait_for_user_selection(_all_blocks, menu_blocks, default)
1467
+ prompt_title = string_send_color(
1468
+ @delegate_object[:prompt_select_block].to_s, :prompt_color_after_script_execution
1469
+ )
1470
+
1471
+ block_menu = prepare_blocks_menu(menu_blocks)
1472
+ if block_menu.empty?
1473
+ return SelectedBlockMenuState.new(nil, MenuState::EXIT)
1474
+ end
1475
+
1476
+ selection_opts = default ? @delegate_object.merge(default: default) : @delegate_object
1477
+ selection_opts.merge!(per_page: @delegate_object[:select_page_height])
1478
+
1479
+ selected_option = select_option_with_metadata(prompt_title, block_menu,
1480
+ selection_opts)
1481
+ determine_block_state(selected_option)
1482
+ end
1483
+
1484
+ # Handles the core logic for generating the command file's metadata and content.
1485
+ def write_command_file(required_lines)
1486
+ return unless @delegate_object[:save_executed_script]
1487
+
1488
+ time_now = Time.now.utc
1489
+ @run_state.saved_script_filename =
1490
+ SavedAsset.script_name(blockname: @delegate_object[:block_name],
1491
+ filename: @delegate_object[:filename],
1492
+ prefix: @delegate_object[:saved_script_filename_prefix],
1493
+ time: time_now)
1494
+
1495
+ @run_state.saved_filespec =
1496
+ File.join(@delegate_object[:saved_script_folder],
1497
+ @run_state.saved_script_filename)
1498
+
1499
+ shebang = if @delegate_object[:shebang]&.present?
1500
+ "#{@delegate_object[:shebang]} #{@delegate_object[:shell]}\n"
1501
+ else
1502
+ ''
1503
+ end
1504
+
1505
+ content = shebang +
1506
+ "# file_name: #{@delegate_object[:filename]}\n" \
1507
+ "# block_name: #{@delegate_object[:block_name]}\n" \
1508
+ "# time: #{time_now}\n" \
1509
+ "#{required_lines.flatten.join("\n")}\n"
1510
+
1511
+ create_and_write_file_with_permissions(
1512
+ @run_state.saved_filespec,
1513
+ content,
1514
+ @delegate_object[:saved_script_chmod]
1515
+ )
1516
+ rescue StandardError
1517
+ error_handler('write_command_file')
1518
+ end
1519
+
1520
+ def write_execution_output_to_file
1521
+ FileUtils.mkdir_p File.dirname(@delegate_object[:logged_stdout_filespec])
1522
+
1523
+ File.write(
1524
+ @delegate_object[:logged_stdout_filespec],
1525
+ ["-STDOUT-\n",
1526
+ format_execution_streams(ExecutionStreams::StdOut),
1527
+ "-STDERR-\n",
1528
+ format_execution_streams(ExecutionStreams::StdErr),
1529
+ "-STDIN-\n",
1530
+ format_execution_streams(ExecutionStreams::StdIn),
1531
+ "\n"].join
1532
+ )
1533
+ end
1534
+
1535
+ # Writes required code blocks to a temporary file and sets an environment variable with its path.
1536
+ #
1537
+ # @param mdoc [Object] The Markdown document object.
1538
+ # @param block_name [String] The name of the block to collect code for.
1539
+ def write_required_blocks_to_temp_file(mdoc, block_name)
1540
+ c1 = if mdoc
1541
+ mdoc.collect_recursively_required_code(
1542
+ block_name,
1543
+ opts: @delegate_object
1544
+ )[:code]
1545
+ else
1546
+ []
1547
+ end
1548
+
1549
+ code_blocks = (read_required_blocks_from_temp_file +
1550
+ c1).join("\n")
1551
+
1552
+ create_temp_file_with_code(code_blocks)
1553
+ end
1554
+
1555
+ # Yields a line as a new block if the selected message type includes :line.
1556
+ # @param [String] line The line to be processed.
1557
+ # @param [Array<Symbol>] selected_messages A list of message types to check.
1558
+ # @param [Proc] block The block to be called with the line data.
1559
+ def yield_line_if_selected(line, selected_messages, &block)
1560
+ return unless block && selected_messages.include?(:line)
1561
+
1562
+ block.call(:line, FCB.new(body: [line]))
1563
+ end
1564
+
1565
+ # Yields to the provided block with specified parameters if certain conditions are met.
1566
+ # The method checks if a block is given, if the selected_messages include :blocks,
1567
+ # and if the fcb_select? method from MarkdownExec::Filter returns true for the given fcb.
1568
+ #
1569
+ # @param fcb [Object] The object to be evaluated and potentially passed to the block.
1570
+ # @param selected_messages [Array<Symbol>] A collection of message types, one of which must be :blocks.
1571
+ # @param block [Block] A block to be called if conditions are met.
1572
+ def yield_to_block_if_applicable(fcb, selected_messages, &block)
1573
+ if block_given? && selected_messages.include?(:blocks) &&
1574
+ MarkdownExec::Filter.fcb_select?(@delegate_object, fcb)
1575
+ block.call :blocks, fcb
1576
+ end
1577
+ end
1578
+ end
1579
+ end
1580
+
1581
+ if $PROGRAM_NAME == __FILE__
1582
+ require 'bundler/setup'
1583
+ Bundler.require(:default)
1584
+
1585
+ require 'minitest/autorun'
1586
+ require 'mocha/minitest'
1587
+
1588
+ module MarkdownExec
1589
+ class TestHashDelegator < Minitest::Test
1590
+ def setup
1591
+ @hd = HashDelegator.new
1592
+ @mdoc = mock('MarkdownDocument')
1593
+ end
1594
+
1595
+ def test_calling_execute_approved_block_calls_command_execute_with_argument_args_value
1596
+ pigeon = 'E'
1597
+ obj = {
1598
+ output_execution_label_format: '',
1599
+ output_execution_label_name_color: 'plain',
1600
+ output_execution_label_value_color: 'plain',
1601
+ s_pass_args: pigeon
1602
+ # shell: 'bash'
1603
+ }
1604
+
1605
+ c = MarkdownExec::HashDelegator.new(obj)
1606
+
1607
+ # Expect that method opts_command_execute is called with argument args having value pigeon
1608
+ c.expects(:command_execute).with(
1609
+ # obj,
1610
+ '',
1611
+ args: pigeon
1612
+ )
1613
+
1614
+ # Call method opts_execute_approved_block
1615
+ c.execute_approved_block([], MarkdownExec::FCB.new)
1616
+ end
1617
+
1618
+ # Test case for empty body
1619
+ def test_handle_link_block_with_empty_body
1620
+ assert_equal LoadFileNextBlock.new(LoadFile::Reuse, ''),
1621
+ @hd.handle_link_block([], nil, false)
1622
+ end
1623
+
1624
+ # Test case for non-empty body without 'file' key
1625
+ def test_handle_link_block_without_file_key
1626
+ body = ["vars:\n KEY: VALUE"]
1627
+ assert_equal LoadFileNextBlock.new(LoadFile::Reuse, ''),
1628
+ @hd.handle_link_block(body, nil, false)
1629
+ end
1630
+
1631
+ # Test case for non-empty body with 'file' key
1632
+ def test_handle_link_block_with_file_key
1633
+ body = ["file: sample_file\nblock: sample_block\nvars:\n KEY: VALUE"]
1634
+ expected_result = LoadFileNextBlock.new(LoadFile::Load,
1635
+ 'sample_block')
1636
+ # mdoc = MDoc.new()
1637
+ assert_equal expected_result,
1638
+ @hd.handle_link_block(body, nil, FCB.new)
1639
+ end
1640
+
1641
+ def test_history_env_state_exist_with_value
1642
+ ENV[MDE_HISTORY_ENV_NAME] = 'history_value'
1643
+ assert @hd.history_env_state_exist?
1644
+ end
1645
+
1646
+ def test_history_env_state_exist_without_value
1647
+ ENV[MDE_HISTORY_ENV_NAME] = ''
1648
+ refute @hd.history_env_state_exist?
1649
+ end
1650
+
1651
+ def test_history_env_state_exist_not_set
1652
+ ENV.delete(MDE_HISTORY_ENV_NAME)
1653
+ refute @hd.history_env_state_exist?
1654
+ end
1655
+
1656
+ def test_indent_all_lines_with_indent
1657
+ body = "Line 1\nLine 2"
1658
+ indent = ' ' # Two spaces
1659
+ expected_result = " Line 1\n Line 2"
1660
+ assert_equal expected_result, @hd.indent_all_lines(body, indent)
1661
+ end
1662
+
1663
+ def test_indent_all_lines_without_indent
1664
+ body = "Line 1\nLine 2"
1665
+ indent = nil
1666
+
1667
+ assert_equal body, @hd.indent_all_lines(body, indent)
1668
+ end
1669
+
1670
+ def test_indent_all_lines_with_empty_indent
1671
+ body = "Line 1\nLine 2"
1672
+ indent = ''
1673
+
1674
+ assert_equal body, @hd.indent_all_lines(body, indent)
1675
+ end
1676
+
1677
+ def test_read_required_blocks_from_temp_file
1678
+ Tempfile.create do |file|
1679
+ file.write("Line 1\nLine 2")
1680
+ file.rewind
1681
+ ENV['MDE_LINK_REQUIRED_FILE'] = file.path
1682
+
1683
+ result = @hd.read_required_blocks_from_temp_file
1684
+ assert_equal ['Line 1', 'Line 2'], result
1685
+ end
1686
+ end
1687
+
1688
+ def test_read_required_blocks_from_temp_file_no_file
1689
+ ENV['MDE_LINK_REQUIRED_FILE'] = nil
1690
+ assert_empty @hd.read_required_blocks_from_temp_file
1691
+ end
1692
+
1693
+ def test_safeval_successful_evaluation
1694
+ assert_equal 4, @hd.safeval('2 + 2')
1695
+ end
1696
+
1697
+ def test_safeval_rescue_from_error
1698
+ @hd.stubs(:error_handler).with('safeval')
1699
+ assert_nil @hd.safeval('invalid code')
1700
+ end
1701
+
1702
+ def test_set_fcb_title
1703
+ # sample input and output data for testing default_block_title_from_body method
1704
+ input_output_data = [
1705
+ {
1706
+ input: MarkdownExec::FCB.new(title: nil,
1707
+ body: ["puts 'Hello, world!'"]),
1708
+ output: "puts 'Hello, world!'"
1709
+ },
1710
+ {
1711
+ input: MarkdownExec::FCB.new(title: '',
1712
+ body: ['def add(x, y)',
1713
+ ' x + y', 'end']),
1714
+ output: "def add(x, y)\n x + y\n end\n"
1715
+ },
1716
+ {
1717
+ input: MarkdownExec::FCB.new(title: 'foo', body: %w[bar baz]),
1718
+ output: 'foo' # expect the title to remain unchanged
1719
+ }
1720
+ ]
1721
+
1722
+ # hd = HashDelegator.new
1723
+
1724
+ # iterate over the input and output data and
1725
+ # assert that the method sets the title as expected
1726
+ input_output_data.each do |data|
1727
+ input = data[:input]
1728
+ output = data[:output]
1729
+ @hd.default_block_title_from_body(input)
1730
+ assert_equal output, input.title
1731
+ end
1732
+ end
1733
+
1734
+ class TestHashDelegatorAppendDivider < Minitest::Test
1735
+ def setup
1736
+ @hd = HashDelegator.new
1737
+ @hd.instance_variable_set(:@delegate_object, {
1738
+ menu_divider_format: 'Format',
1739
+ menu_initial_divider: 'Initial Divider',
1740
+ menu_final_divider: 'Final Divider',
1741
+ menu_divider_color: :color
1742
+ })
1743
+ @hd.stubs(:string_send_color).returns('Formatted Divider')
1744
+ @hd.stubs(:safeval).returns('Safe Value')
1745
+ end
1746
+
1747
+ def test_append_divider_initial
1748
+ menu_blocks = []
1749
+ @hd.append_divider(menu_blocks, :initial)
1750
+
1751
+ assert_equal 1, menu_blocks.size
1752
+ assert_equal 'Formatted Divider', menu_blocks.first.dname
1753
+ end
1754
+
1755
+ def test_append_divider_final
1756
+ menu_blocks = []
1757
+ @hd.append_divider(menu_blocks, :final)
1758
+
1759
+ assert_equal 1, menu_blocks.size
1760
+ assert_equal 'Formatted Divider', menu_blocks.last.dname
1761
+ end
1762
+
1763
+ def test_append_divider_without_format
1764
+ @hd.instance_variable_set(:@delegate_object, {})
1765
+ menu_blocks = []
1766
+ @hd.append_divider(menu_blocks, :initial)
1767
+
1768
+ assert_empty menu_blocks
1769
+ end
1770
+ end # class TestHashDelegator
1771
+
1772
+ class TestHashDelegatorBlockFind < Minitest::Test
1773
+ def setup
1774
+ @hd = HashDelegator.new
1775
+ end
1776
+
1777
+ def test_block_find_with_match
1778
+ blocks = [{ key: 'value1' }, { key: 'value2' }]
1779
+ result = @hd.block_find(blocks, :key, 'value1')
1780
+ assert_equal({ key: 'value1' }, result)
1781
+ end
1782
+
1783
+ def test_block_find_without_match
1784
+ blocks = [{ key: 'value1' }, { key: 'value2' }]
1785
+ result = @hd.block_find(blocks, :key, 'value3')
1786
+ assert_nil result
1787
+ end
1788
+
1789
+ def test_block_find_with_default
1790
+ blocks = [{ key: 'value1' }, { key: 'value2' }]
1791
+ result = @hd.block_find(blocks, :key, 'value3', 'default')
1792
+ assert_equal 'default', result
1793
+ end
1794
+ end # class TestHashDelegator
1795
+
1796
+ class TestHashDelegatorBlocksFromNestedFiles < Minitest::Test
1797
+ def setup
1798
+ @hd = HashDelegator.new
1799
+ @hd.stubs(:iter_blocks_from_nested_files).yields(:blocks, FCB.new)
1800
+ @hd.stubs(:get_block_summary).returns(FCB.new)
1801
+ @hd.stubs(:create_and_add_chrome_blocks)
1802
+ @hd.instance_variable_set(:@delegate_object, {})
1803
+ @hd.stubs(:error_handler)
1804
+ end
1805
+
1806
+ def test_blocks_from_nested_files
1807
+ result = @hd.blocks_from_nested_files
1808
+
1809
+ assert_kind_of Array, result
1810
+ assert_kind_of FCB, result.first
1811
+ end
1812
+
1813
+ def test_blocks_from_nested_files_with_no_chrome
1814
+ @hd.instance_variable_set(:@delegate_object, { no_chrome: true })
1815
+ @hd.expects(:create_and_add_chrome_blocks).never
1816
+
1817
+ result = @hd.blocks_from_nested_files
1818
+
1819
+ assert_kind_of Array, result
1820
+ end
1821
+ end # class TestHashDelegator
1822
+
1823
+ class TestHashDelegatorCollectRequiredCodeLines < Minitest::Test
1824
+ def setup
1825
+ @hd = HashDelegator.new
1826
+ @hd.instance_variable_set(:@delegate_object, {})
1827
+ @mdoc = mock('YourMDocClass')
1828
+ @selected = { shell: BlockType::VARS, body: ['key: value'] }
1829
+ @hd.stubs(:read_required_blocks_from_temp_file).returns([])
1830
+ @hd.stubs(:string_send_color)
1831
+ @hd.stubs(:print)
1832
+ end
1833
+
1834
+ def test_collect_required_code_lines_with_vars
1835
+ YAML.stubs(:load).returns({ 'key' => 'value' })
1836
+ @mdoc.stubs(:collect_recursively_required_code).returns({ code: ['code line'] })
1837
+ ENV.stubs(:[]=)
1838
+
1839
+ result = @hd.collect_required_code_lines(@mdoc, @selected)
1840
+
1841
+ assert_equal ['code line'], result
1842
+ end
1843
+ end # class TestHashDelegator
1844
+
1845
+ class TestHashDelegatorCommandOrUserSelectedBlock < Minitest::Test
1846
+ def setup
1847
+ @hd = HashDelegator.new
1848
+ @hd.instance_variable_set(:@delegate_object, {})
1849
+ @hd.stubs(:error_handler)
1850
+ @hd.stubs(:wait_for_user_selected_block)
1851
+ end
1852
+
1853
+ def test_command_selected_block
1854
+ all_blocks = [{ oname: 'block1' }, { oname: 'block2' }]
1855
+ @hd.instance_variable_set(:@delegate_object,
1856
+ { block_name: 'block1' })
1857
+
1858
+ result = @hd.command_or_user_selected_block(all_blocks, [], nil)
1859
+
1860
+ assert_equal all_blocks.first, result.block
1861
+ assert_nil result.state
1862
+ end
1863
+
1864
+ def test_user_selected_block
1865
+ block_state = SelectedBlockMenuState.new({ oname: 'block2' },
1866
+ :some_state)
1867
+ @hd.stubs(:wait_for_user_selected_block).returns(block_state)
1868
+
1869
+ result = @hd.command_or_user_selected_block([], [], nil)
1870
+
1871
+ assert_equal block_state.block, result.block
1872
+ assert_equal :some_state, result.state
1873
+ end
1874
+ end # class TestHashDelegator
1875
+
1876
+ class TestHashDelegatorCountBlockInFilename < Minitest::Test
1877
+ def setup
1878
+ @hd = HashDelegator.new
1879
+ @hd.instance_variable_set(:@delegate_object,
1880
+ { fenced_start_and_end_regex: '^```',
1881
+ filename: '/path/to/file' })
1882
+ @hd.stubs(:cfile).returns(mock('cfile'))
1883
+ end
1884
+
1885
+ def test_count_blocks_in_filename
1886
+ file_content = ["```ruby\n", "puts 'Hello'\n", "```\n",
1887
+ "```python\n", "print('Hello')\n", "```\n"]
1888
+ @hd.cfile.stubs(:readlines).with('/path/to/file').returns(file_content)
1889
+
1890
+ count = @hd.count_blocks_in_filename
1891
+
1892
+ assert_equal 2, count
1893
+ end
1894
+
1895
+ def test_count_blocks_in_filename_with_no_matches
1896
+ file_content = ["puts 'Hello'\n", "print('Hello')\n"]
1897
+ @hd.cfile.stubs(:readlines).with('/path/to/file').returns(file_content)
1898
+
1899
+ count = @hd.count_blocks_in_filename
1900
+
1901
+ assert_equal 0, count
1902
+ end
1903
+ end # class TestHashDelegator
1904
+
1905
+ class TestHashDelegatorCreateAndWriteFile < Minitest::Test
1906
+ def setup
1907
+ @hd = HashDelegator.new
1908
+ @hd.stubs(:error_handler)
1909
+ FileUtils.stubs(:mkdir_p)
1910
+ File.stubs(:write)
1911
+ File.stubs(:chmod)
1912
+ end
1913
+
1914
+ def test_create_and_write_file_with_permissions
1915
+ file_path = '/path/to/file'
1916
+ content = 'sample content'
1917
+ chmod_value = 0o644
1918
+
1919
+ FileUtils.expects(:mkdir_p).with('/path/to').once
1920
+ File.expects(:write).with(file_path, content).once
1921
+ File.expects(:chmod).with(chmod_value, file_path).once
1922
+
1923
+ @hd.create_and_write_file_with_permissions(file_path, content,
1924
+ chmod_value)
1925
+
1926
+ assert true # Placeholder for actual test assertions
1927
+ end
1928
+
1929
+ def test_create_and_write_file_without_chmod
1930
+ file_path = '/path/to/file'
1931
+ content = 'sample content'
1932
+ chmod_value = 0
1933
+
1934
+ FileUtils.expects(:mkdir_p).with('/path/to').once
1935
+ File.expects(:write).with(file_path, content).once
1936
+ File.expects(:chmod).never
1937
+
1938
+ @hd.create_and_write_file_with_permissions(file_path, content,
1939
+ chmod_value)
1940
+
1941
+ assert true # Placeholder for actual test assertions
1942
+ end
1943
+ end # class TestHashDelegator
1944
+
1945
+ class TestHashDelegatorCreateTempFile < Minitest::Test
1946
+ def setup
1947
+ @hd = HashDelegator.new
1948
+ @temp_file_path = '/tmp/tempfile'
1949
+ end
1950
+
1951
+ def test_create_temp_file_with_code
1952
+ Dir::Tmpname.stubs(:create).returns(@temp_file_path)
1953
+ File.stubs(:write).with(@temp_file_path, 'code_blocks')
1954
+ # ENV.expects(:[]=).with('MDE_LINK_REQUIRED_FILE', @temp_file_path)
1955
+
1956
+ @hd.create_temp_file_with_code('code_blocks')
1957
+
1958
+ assert true # Placeholder for actual test assertions
1959
+ end
1960
+ end # class TestHashDelegator
1961
+
1962
+ class TestHashDelegatorDeleteRequiredTempFile < Minitest::Test
1963
+ def setup
1964
+ @hd = HashDelegator.new
1965
+ @hd.stubs(:error_handler)
1966
+ @hd.stubs(:clear_required_file)
1967
+ FileUtils.stubs(:rm_f)
1968
+ end
1969
+
1970
+ def test_delete_required_temp_file_with_existing_file
1971
+ ENV.stubs(:fetch).with('MDE_LINK_REQUIRED_FILE',
1972
+ nil).returns('/path/to/temp_file')
1973
+ FileUtils.expects(:rm_f).with('/path/to/temp_file').once
1974
+ @hd.expects(:clear_required_file).once
1975
+
1976
+ @hd.delete_required_temp_file
1977
+
1978
+ assert true # Placeholder for actual test assertions
1979
+ end
1980
+
1981
+ def test_delete_required_temp_file_with_no_file
1982
+ ENV.stubs(:fetch).with('MDE_LINK_REQUIRED_FILE', nil).returns(nil)
1983
+ FileUtils.expects(:rm_f).never
1984
+ @hd.expects(:clear_required_file).never
1985
+
1986
+ @hd.delete_required_temp_file
1987
+
1988
+ assert true # Placeholder for actual test assertions
1989
+ end
1990
+
1991
+ def test_delete_required_temp_file_with_error
1992
+ ENV.stubs(:fetch).with('MDE_LINK_REQUIRED_FILE',
1993
+ nil).returns('/path/to/temp_file')
1994
+ FileUtils.stubs(:rm_f).raises(StandardError)
1995
+ @hd.expects(:error_handler).with('delete_required_temp_file').once
1996
+
1997
+ @hd.delete_required_temp_file
1998
+
1999
+ assert true # Placeholder for actual test assertions
2000
+ end
2001
+ end # class TestHashDelegator
2002
+
2003
+ class TestHashDelegatorDetermineBlockState < Minitest::Test
2004
+ def setup
2005
+ @hd = HashDelegator.new
2006
+ @hd.stubs(:menu_chrome_formatted_option).returns('Formatted Option')
2007
+ end
2008
+
2009
+ def test_determine_block_state_exit
2010
+ selected_option = { oname: 'Formatted Option' }
2011
+ @hd.stubs(:menu_chrome_formatted_option).with(:menu_option_exit_name).returns('Formatted Option')
2012
+
2013
+ result = @hd.determine_block_state(selected_option)
2014
+
2015
+ assert_equal MenuState::EXIT, result.state
2016
+ assert_nil result.block
2017
+ end
2018
+
2019
+ def test_determine_block_state_back
2020
+ selected_option = { oname: 'Formatted Back Option' }
2021
+ @hd.stubs(:menu_chrome_formatted_option).with(:menu_option_back_name).returns('Formatted Back Option')
2022
+ result = @hd.determine_block_state(selected_option)
2023
+
2024
+ assert_equal MenuState::BACK, result.state
2025
+ assert_equal selected_option, result.block
2026
+ end
2027
+
2028
+ def test_determine_block_state_continue
2029
+ selected_option = { oname: 'Other Option' }
2030
+
2031
+ result = @hd.determine_block_state(selected_option)
2032
+
2033
+ assert_equal MenuState::CONTINUE, result.state
2034
+ assert_equal selected_option, result.block
2035
+ end
2036
+ end # class TestHashDelegator
2037
+
2038
+ class TestHashDelegatorDisplayRequiredCode < Minitest::Test
2039
+ def setup
2040
+ @hd = HashDelegator.new
2041
+ @hd.instance_variable_set(:@fout, mock('fout'))
2042
+ @hd.instance_variable_set(:@delegate_object, {})
2043
+ @hd.stubs(:string_send_color)
2044
+ end
2045
+
2046
+ def test_display_required_code
2047
+ required_lines = %w[line1 line2]
2048
+ @hd.instance_variable_get(:@delegate_object).stubs(:[]).with(:script_preview_head).returns('Header')
2049
+ @hd.instance_variable_get(:@delegate_object).stubs(:[]).with(:script_preview_tail).returns('Footer')
2050
+ @hd.instance_variable_get(:@fout).expects(:fout).times(4)
2051
+
2052
+ @hd.display_required_code(required_lines)
2053
+
2054
+ # Verifying that fout is called for each line and for header & footer
2055
+ assert true # Placeholder for actual test assertions
2056
+ end
2057
+ end # class TestHashDelegator
2058
+
2059
+ class TestHashDelegatorFetchColor < Minitest::Test
2060
+ def setup
2061
+ @hd = HashDelegator.new
2062
+ @hd.instance_variable_set(:@delegate_object, {})
2063
+ end
2064
+
2065
+ def test_fetch_color_with_valid_data
2066
+ @hd.instance_variable_get(:@delegate_object).stubs(:fetch).with(
2067
+ :execution_report_preview_head, ''
2068
+ ).returns('Data String')
2069
+ @hd.stubs(:string_send_color).with('Data String',
2070
+ :execution_report_preview_frame_color).returns('Colored Data String')
2071
+
2072
+ result = @hd.fetch_color
2073
+
2074
+ assert_equal 'Colored Data String', result
2075
+ end
2076
+
2077
+ def test_fetch_color_with_missing_data
2078
+ @hd.instance_variable_get(:@delegate_object).stubs(:fetch).with(
2079
+ :execution_report_preview_head, ''
2080
+ ).returns('')
2081
+ @hd.stubs(:string_send_color).with('',
2082
+ :execution_report_preview_frame_color).returns('Default Colored String')
2083
+
2084
+ result = @hd.fetch_color
2085
+
2086
+ assert_equal 'Default Colored String', result
2087
+ end
2088
+ end # class TestHashDelegator
2089
+
2090
+ class TestHashDelegatorFormatReferencesSendColor < Minitest::Test
2091
+ def setup
2092
+ @hd = HashDelegator.new
2093
+ @hd.instance_variable_set(:@delegate_object, {})
2094
+ end
2095
+
2096
+ def test_format_references_send_color_with_valid_data
2097
+ @hd.instance_variable_get(:@delegate_object).stubs(:fetch).with(
2098
+ :output_execution_label_format, ''
2099
+ ).returns('Formatted: %{key}')
2100
+ @hd.stubs(:string_send_color).returns('Colored String')
2101
+
2102
+ result = @hd.format_references_send_color(context: { key: 'value' },
2103
+ color_sym: :execution_report_preview_frame_color)
2104
+
2105
+ assert_equal 'Colored String', result
2106
+ end
2107
+
2108
+ def test_format_references_send_color_with_missing_format
2109
+ @hd.instance_variable_get(:@delegate_object).stubs(:fetch).with(
2110
+ :output_execution_label_format, ''
2111
+ ).returns('')
2112
+ @hd.stubs(:string_send_color).returns('Default Colored String')
2113
+
2114
+ result = @hd.format_references_send_color(context: { key: 'value' },
2115
+ color_sym: :execution_report_preview_frame_color)
2116
+
2117
+ assert_equal 'Default Colored String', result
2118
+ end
2119
+ end # class TestHashDelegator
2120
+
2121
+ class TestHashDelegatorFormatExecutionStreams < Minitest::Test
2122
+ def setup
2123
+ @hd = HashDelegator.new
2124
+ @hd.instance_variable_set(:@run_state, mock('run_state'))
2125
+ end
2126
+
2127
+ def test_format_execution_streams_with_valid_key
2128
+ @hd.instance_variable_get(:@run_state).stubs(:files).returns({ stdout: %w[
2129
+ output1 output2
2130
+ ] })
2131
+
2132
+ result = @hd.format_execution_streams(:stdout)
2133
+
2134
+ assert_equal 'output1output2', result
2135
+ end
2136
+
2137
+ def test_format_execution_streams_with_empty_key
2138
+ @hd.instance_variable_get(:@run_state).stubs(:files).returns({})
2139
+
2140
+ result = @hd.format_execution_streams(:stderr)
2141
+
2142
+ assert_equal '', result
2143
+ end
2144
+
2145
+ def test_format_execution_streams_with_nil_files
2146
+ @hd.instance_variable_get(:@run_state).stubs(:files).returns(nil)
2147
+
2148
+ result = @hd.format_execution_streams(:stdin)
2149
+
2150
+ assert_equal '', result
2151
+ end
2152
+ end # class TestHashDelegator
2153
+
2154
+ class TestHashDelegatorHandleBackLink < Minitest::Test
2155
+ def setup
2156
+ @hd = HashDelegator.new
2157
+ @hd.stubs(:history_state_pop)
2158
+ end
2159
+
2160
+ def test_handle_back_link
2161
+ # Verifying that history_state_pop is called
2162
+ @hd.expects(:history_state_pop).once
2163
+
2164
+ result = @hd.handle_back_link
2165
+
2166
+ # Asserting the result is an instance of LoadFileNextBlock
2167
+ assert_instance_of LoadFileNextBlock, result
2168
+ assert_equal LoadFile::Load, result.load_file
2169
+ assert_equal '', result.next_block
2170
+ end
2171
+ end # class TestHashDelegator
2172
+
2173
+ class TestHashDelegatorHandleBlockState < Minitest::Test
2174
+ def setup
2175
+ @hd = HashDelegator.new
2176
+ @mock_block_state = mock('block_state')
2177
+ end
2178
+
2179
+ def test_handle_block_state_with_back
2180
+ @mock_block_state.stubs(:state).returns(MenuState::BACK)
2181
+ @mock_block_state.stubs(:block).returns({ dname: 'sample_block' })
2182
+
2183
+ @hd.handle_block_state(@mock_block_state)
2184
+
2185
+ assert_equal 'sample_block',
2186
+ @hd.instance_variable_get(:@delegate_object)[:block_name]
2187
+ assert @hd.instance_variable_get(:@menu_user_clicked_back_link)
2188
+ end
2189
+
2190
+ def test_handle_block_state_with_continue
2191
+ @mock_block_state.stubs(:state).returns(MenuState::CONTINUE)
2192
+ @mock_block_state.stubs(:block).returns({ dname: 'another_block' })
2193
+
2194
+ @hd.handle_block_state(@mock_block_state)
2195
+
2196
+ assert_equal 'another_block',
2197
+ @hd.instance_variable_get(:@delegate_object)[:block_name]
2198
+ refute @hd.instance_variable_get(:@menu_user_clicked_back_link)
2199
+ end
2200
+
2201
+ def test_handle_block_state_with_other
2202
+ @mock_block_state.stubs(:state).returns(nil) # MenuState::OTHER
2203
+ @mock_block_state.stubs(:block).returns({ dname: 'other_block' })
2204
+
2205
+ @hd.handle_block_state(@mock_block_state)
2206
+
2207
+ assert_nil @hd.instance_variable_get(:@delegate_object)[:block_name]
2208
+ assert_nil @hd.instance_variable_get(:@menu_user_clicked_back_link)
2209
+ end
2210
+ end # class TestHashDelegator
2211
+
2212
+ class TestHashDelegatorHandleGenericBlock < Minitest::Test
2213
+ def setup
2214
+ @hd = HashDelegator.new
2215
+ @mock_document = mock('MarkdownDocument')
2216
+ @selected_item = mock('FCB')
2217
+ end
2218
+
2219
+ def test_handle_generic_block_without_user_approval
2220
+ # Mock the delegate object configuration
2221
+ @hd.instance_variable_set(:@delegate_object,
2222
+ { output_script: false,
2223
+ user_must_approve: false })
2224
+
2225
+ # Test the method without user approval
2226
+ # Expectations and assertions go here
2227
+ end
2228
+
2229
+ def test_handle_generic_block_with_user_approval
2230
+ # Mock the delegate object configuration
2231
+ @hd.instance_variable_set(:@delegate_object,
2232
+ { output_script: false,
2233
+ user_must_approve: true })
2234
+
2235
+ # Test the method with user approval
2236
+ # Expectations and assertions go here
2237
+ end
2238
+
2239
+ def test_handle_generic_block_with_output_script
2240
+ # Mock the delegate object configuration
2241
+ @hd.instance_variable_set(:@delegate_object,
2242
+ { output_script: true,
2243
+ user_must_approve: false })
2244
+
2245
+ # Test the method with output script option
2246
+ # Expectations and assertions go here
2247
+ end
2248
+ end
2249
+
2250
+ class TestHashDelegatorHandleOptsBlock < Minitest::Test
2251
+ def setup
2252
+ @hd = HashDelegator.new
2253
+ @hd.instance_variable_set(:@delegate_object,
2254
+ { menu_opts_set_format: 'Option: %<key>s, Value: %<value>s',
2255
+ menu_opts_set_color: :blue })
2256
+ @hd.stubs(:string_send_color)
2257
+ @hd.stubs(:print)
2258
+ end
2259
+
2260
+ def test_handle_opts_block
2261
+ selected = { body: ['option1: value1'] }
2262
+ tgt2 = {}
2263
+
2264
+ result = @hd.handle_opts_block(selected, tgt2)
2265
+
2266
+ assert_instance_of LoadFileNextBlock, result
2267
+ assert_equal 'value1',
2268
+ @hd.instance_variable_get(:@delegate_object)[:option1]
2269
+ assert_equal 'value1', tgt2[:option1]
2270
+ end
2271
+
2272
+ def test_handle_opts_block_without_format
2273
+ selected = { body: ['option2: value2'] }
2274
+ @hd.instance_variable_set(:@delegate_object, {})
2275
+
2276
+ result = @hd.handle_opts_block(selected)
2277
+
2278
+ assert_instance_of LoadFileNextBlock, result
2279
+ assert_equal 'value2',
2280
+ @hd.instance_variable_get(:@delegate_object)[:option2]
2281
+ end
2282
+
2283
+ # Additional test cases can be added to cover more scenarios and edge cases.
2284
+ end
2285
+
2286
+ # require 'stringio'
2287
+
2288
+ class TestHashDelegatorHandleStream < Minitest::Test
2289
+ def setup
2290
+ @hd = HashDelegator.new
2291
+ @hd.instance_variable_set(:@run_state,
2292
+ OpenStruct.new(files: { stdout: [] }))
2293
+ @hd.instance_variable_set(:@delegate_object,
2294
+ { output_stdout: true })
2295
+ end
2296
+
2297
+ def test_handle_stream
2298
+ stream = StringIO.new("line 1\nline 2\n")
2299
+ file_type = :stdout
2300
+
2301
+ Thread.new { @hd.handle_stream(stream, file_type) }
2302
+
2303
+ @hd.wait_for_stream_processing
2304
+
2305
+ assert_equal ['line 1', 'line 2'],
2306
+ @hd.instance_variable_get(:@run_state).files[:stdout]
2307
+ end
2308
+
2309
+ def test_handle_stream_with_io_error
2310
+ stream = StringIO.new("line 1\nline 2\n")
2311
+ file_type = :stdout
2312
+ stream.stubs(:each_line).raises(IOError)
2313
+
2314
+ Thread.new { @hd.handle_stream(stream, file_type) }
2315
+
2316
+ @hd.wait_for_stream_processing
2317
+
2318
+ assert_equal [],
2319
+ @hd.instance_variable_get(:@run_state).files[:stdout]
2320
+ end
2321
+ end
2322
+
2323
+ class TestHashDelegatorHistoryStatePartition < Minitest::Test
2324
+ def setup
2325
+ @hd = HashDelegator.new
2326
+ @hd.instance_variable_set(:@delegate_object, {
2327
+ history_document_separator: '|'
2328
+ })
2329
+ end
2330
+
2331
+ def test_history_state_partition_with_value
2332
+ ENV[MDE_HISTORY_ENV_NAME] = 'part1|part2'
2333
+
2334
+ result = @hd.history_state_partition
2335
+ assert_equal({ unit: 'part1', rest: 'part2' }, result)
2336
+ end
2337
+
2338
+ def test_history_state_partition_with_no_separator
2339
+ ENV[MDE_HISTORY_ENV_NAME] = 'onlypart'
2340
+
2341
+ result = @hd.history_state_partition
2342
+ assert_equal({ unit: 'onlypart', rest: '' }, result)
2343
+ end
2344
+
2345
+ def test_history_state_partition_with_empty_env
2346
+ ENV[MDE_HISTORY_ENV_NAME] = ''
2347
+
2348
+ result = @hd.history_state_partition
2349
+ assert_equal({ unit: '', rest: '' }, result)
2350
+ end
2351
+
2352
+ # Additional test cases can be added to cover more scenarios and edge cases.
2353
+ end
2354
+
2355
+ class TestHashDelegatorHistoryStatePop < Minitest::Test
2356
+ def setup
2357
+ @hd = HashDelegator.new
2358
+ @hd.instance_variable_set(:@delegate_object,
2359
+ { filename: 'initial.md' })
2360
+ @hd.instance_variable_set(:@run_state,
2361
+ OpenStruct.new(link_history: [{ block_name: 'block1',
2362
+ filename: 'file1.md' }]))
2363
+ @hd.stubs(:history_state_partition).returns({ unit: 'file2.md',
2364
+ rest: 'history_data' })
2365
+ @hd.stubs(:delete_required_temp_file)
2366
+ end
2367
+
2368
+ def test_history_state_pop
2369
+ ENV[MDE_HISTORY_ENV_NAME] = 'some_history'
2370
+
2371
+ @hd.history_state_pop
2372
+
2373
+ assert_equal 'file2.md',
2374
+ @hd.instance_variable_get(:@delegate_object)[:filename]
2375
+ assert_equal 'history_data',
2376
+ ENV.fetch(MDE_HISTORY_ENV_NAME, nil)
2377
+ assert_empty @hd.instance_variable_get(:@run_state).link_history
2378
+ end
2379
+
2380
+ # Additional test cases can be added to cover more scenarios and edge cases.
2381
+ end
2382
+
2383
+ class TestHashDelegatorHistoryStatePush < Minitest::Test
2384
+ def setup
2385
+ @hd = HashDelegator.new
2386
+ @hd.instance_variable_set(:@delegate_object, {
2387
+ filename: 'test.md',
2388
+ block_name: 'test_block',
2389
+ history_document_separator: '||'
2390
+ })
2391
+ @hd.instance_variable_set(:@run_state,
2392
+ OpenStruct.new(link_history: []))
2393
+ @hd.stubs(:write_required_blocks_to_temp_file)
2394
+ end
2395
+
2396
+ def test_history_state_push
2397
+ mdoc = 'markdown content'
2398
+ data_file = 'data.md'
2399
+ selected = { oname: 'selected_block' }
2400
+
2401
+ ENV[MDE_HISTORY_ENV_NAME] = 'existing_history'
2402
+
2403
+ @hd.history_state_push(mdoc, data_file, selected)
2404
+
2405
+ assert_equal 'data.md',
2406
+ @hd.instance_variable_get(:@delegate_object)[:filename]
2407
+ assert_equal 'test.md||existing_history',
2408
+ ENV.fetch(MDE_HISTORY_ENV_NAME, nil)
2409
+ assert_includes @hd.instance_variable_get(:@run_state).link_history,
2410
+ { block_name: 'selected_block',
2411
+ filename: 'data.md' }
2412
+ end
2413
+
2414
+ # Additional test cases can be added to cover more scenarios and edge cases.
2415
+ end
2416
+
2417
+ class TestHashDelegatorIterBlocksFromNestedFiles < Minitest::Test
2418
+ def setup
2419
+ @hd = HashDelegator.new
2420
+ @hd.instance_variable_set(:@delegate_object,
2421
+ { filename: 'test.md' })
2422
+ @hd.stubs(:check_file_existence).with('test.md').returns(true)
2423
+ @hd.stubs(:initial_state).returns({})
2424
+ @hd.stubs(:cfile).returns(Minitest::Mock.new)
2425
+ @hd.stubs(:update_line_and_block_state)
2426
+ end
2427
+
2428
+ def test_iter_blocks_from_nested_files
2429
+ @hd.cfile.expect(:readlines, ['line 1', 'line 2'], ['test.md'])
2430
+ selected_messages = ['filtered message']
2431
+
2432
+ result = @hd.iter_blocks_from_nested_files { selected_messages }
2433
+ assert_equal ['line 1', 'line 2'], result
2434
+
2435
+ @hd.cfile.verify
2436
+ end
2437
+
2438
+ def test_iter_blocks_from_nested_files_with_no_file
2439
+ @hd.stubs(:check_file_existence).with('test.md').returns(false)
2440
+
2441
+ assert_nil(@hd.iter_blocks_from_nested_files do
2442
+ ['filtered message']
2443
+ end)
2444
+ end
2445
+ end
2446
+
2447
+ class TestHashDelegatorLoadAutoBlocks < Minitest::Test
2448
+ def setup
2449
+ @hd = HashDelegator.new
2450
+ @hd.instance_variable_set(:@delegate_object, {
2451
+ document_load_opts_block_name: 'load_block',
2452
+ s_most_recent_filename: 'old_file',
2453
+ filename: 'new_file'
2454
+ })
2455
+ @hd.stubs(:block_find).returns({}) # Assuming it returns a block
2456
+ @hd.stubs(:handle_opts_block)
2457
+ end
2458
+
2459
+ def test_load_auto_blocks_with_new_filename
2460
+ assert @hd.load_auto_blocks([])
2461
+ end
2462
+
2463
+ def test_load_auto_blocks_with_same_filename
2464
+ @hd.instance_variable_set(:@delegate_object, {
2465
+ document_load_opts_block_name: 'load_block',
2466
+ s_most_recent_filename: 'new_file',
2467
+ filename: 'new_file'
2468
+ })
2469
+ assert_nil @hd.load_auto_blocks([])
2470
+ end
2471
+
2472
+ def test_load_auto_blocks_without_block_name
2473
+ @hd.instance_variable_set(:@delegate_object, {
2474
+ document_load_opts_block_name: nil,
2475
+ s_most_recent_filename: 'old_file',
2476
+ filename: 'new_file'
2477
+ })
2478
+ assert_nil @hd.load_auto_blocks([])
2479
+ end
2480
+ end
2481
+
2482
+ class TestHashDelegatorMenuChromeColoredOption < Minitest::Test
2483
+ def setup
2484
+ @hd = HashDelegator.new
2485
+ @hd.instance_variable_set(:@delegate_object, {
2486
+ menu_option_back_name: 'Back',
2487
+ menu_chrome_color: :red,
2488
+ menu_chrome_format: '-- %s --'
2489
+ })
2490
+ @hd.stubs(:menu_chrome_formatted_option).with(:menu_option_back_name).returns('-- Back --')
2491
+ @hd.stubs(:string_send_color).with('-- Back --',
2492
+ :menu_chrome_color).returns('-- Back --'.red)
2493
+ end
2494
+
2495
+ def test_menu_chrome_colored_option_with_color
2496
+ assert_equal '-- Back --'.red,
2497
+ @hd.menu_chrome_colored_option(:menu_option_back_name)
2498
+ end
2499
+
2500
+ def test_menu_chrome_colored_option_without_color
2501
+ @hd.instance_variable_set(:@delegate_object,
2502
+ { menu_option_back_name: 'Back' })
2503
+ assert_equal '-- Back --',
2504
+ @hd.menu_chrome_colored_option(:menu_option_back_name)
2505
+ end
2506
+ end
2507
+
2508
+ class TestHashDelegatorMenuChromeFormattedOptionWithoutFormat < Minitest::Test
2509
+ def setup
2510
+ @hd = HashDelegator.new
2511
+ @hd.instance_variable_set(:@delegate_object, {
2512
+ menu_option_back_name: "'Back'",
2513
+ menu_chrome_format: '-- %s --'
2514
+ })
2515
+ @hd.stubs(:safeval).with("'Back'").returns('Back')
2516
+ end
2517
+
2518
+ def test_menu_chrome_formatted_option_with_format
2519
+ assert_equal '-- Back --',
2520
+ @hd.menu_chrome_formatted_option(:menu_option_back_name)
2521
+ end
2522
+
2523
+ def test_menu_chrome_formatted_option_without_format
2524
+ @hd.instance_variable_set(:@delegate_object,
2525
+ { menu_option_back_name: "'Back'" })
2526
+ assert_equal 'Back',
2527
+ @hd.menu_chrome_formatted_option(:menu_option_back_name)
2528
+ end
2529
+ end
2530
+
2531
+ class TestHashDelegatorStartFencedBlock < Minitest::Test
2532
+ def setup
2533
+ @hd = HashDelegator.new({
2534
+ block_name_wrapper_match: 'WRAPPER_REGEX',
2535
+ block_calls_scan: 'CALLS_REGEX'
2536
+ })
2537
+ end
2538
+
2539
+ def test_start_fenced_block
2540
+ line = '```fenced'
2541
+ headings = ['Heading 1']
2542
+ regex = /```(?<name>\w+)(?<rest>.*)/
2543
+
2544
+ fcb = @hd.start_fenced_block(line, headings, regex)
2545
+
2546
+ assert_instance_of MarkdownExec::FCB, fcb
2547
+ assert_equal headings, fcb.headings
2548
+ assert_equal 'fenced', fcb.dname
2549
+ end
2550
+ end
2551
+
2552
+ class TestHashDelegatorStringSendColor < Minitest::Test
2553
+ def setup
2554
+ @hd = HashDelegator.new
2555
+ @hd.instance_variable_set(:@delegate_object,
2556
+ { red: 'red', green: 'green' })
2557
+ end
2558
+
2559
+ def test_string_send_color
2560
+ assert_equal 'Hello'.red, @hd.string_send_color('Hello', :red)
2561
+ assert_equal 'World'.green,
2562
+ @hd.string_send_color('World', :green)
2563
+ assert_equal 'Default'.plain,
2564
+ @hd.string_send_color('Default', :blue)
2565
+ end
2566
+ end
2567
+
2568
+ def test_yield_line_if_selected_with_line
2569
+ block_called = false
2570
+ @hd.yield_line_if_selected('Test line', [:line]) do |type, content|
2571
+ block_called = true
2572
+ assert_equal :line, type
2573
+ assert_equal 'Test line', content.body[0]
2574
+ end
2575
+ assert block_called
2576
+ end
2577
+
2578
+ def test_yield_line_if_selected_without_line
2579
+ block_called = false
2580
+ @hd.yield_line_if_selected('Test line', [:other]) do |_|
2581
+ block_called = true
2582
+ end
2583
+ refute block_called
2584
+ end
2585
+
2586
+ def test_yield_line_if_selected_without_block
2587
+ result = @hd.yield_line_if_selected('Test line', [:line])
2588
+ assert_nil result
2589
+ end
2590
+ end # class TestHashDelegator
2591
+
2592
+ class TestHashDelegator < Minitest::Test
2593
+ def setup
2594
+ @hd = HashDelegator.new
2595
+ @hd.instance_variable_set(:@delegate_object, {
2596
+ heading1_match: '^# (?<name>.+)$',
2597
+ heading2_match: '^## (?<name>.+)$',
2598
+ heading3_match: '^### (?<name>.+)$'
2599
+ })
2600
+ end
2601
+
2602
+ def test_update_document_headings
2603
+ assert_equal(['Heading 1'],
2604
+ @hd.update_document_headings('# Heading 1', []))
2605
+ assert_equal(['Heading 1', 'Heading 2'],
2606
+ @hd.update_document_headings('## Heading 2',
2607
+ ['Heading 1']))
2608
+ assert_equal(['Heading 1', 'Heading 2', 'Heading 3'],
2609
+ @hd.update_document_headings('### Heading 3',
2610
+ ['Heading 1', 'Heading 2']))
2611
+ assert_equal([], @hd.update_document_headings('Regular text', []))
2612
+ end
2613
+ end
2614
+
2615
+ class TestHashDelegatorUpdateMenuAttribYieldSelectedWithBody < Minitest::Test
2616
+ def setup
2617
+ @hd = HashDelegator.new
2618
+ @fcb = mock('Fcb')
2619
+ @fcb.stubs(:body).returns(true)
2620
+ @hd.stubs(:initialize_fcb_names)
2621
+ @hd.stubs(:default_block_title_from_body)
2622
+ @hd.stubs(:yield_to_block_if_applicable)
2623
+ end
2624
+
2625
+ def test_update_menu_attrib_yield_selected_with_body
2626
+ @hd.expects(:initialize_fcb_names).with(@fcb)
2627
+ @hd.expects(:default_block_title_from_body).with(@fcb)
2628
+ @hd.expects(:yield_to_block_if_applicable).with(@fcb, [:some_message])
2629
+
2630
+ @hd.update_menu_attrib_yield_selected(@fcb, [:some_message])
2631
+ end
2632
+
2633
+ def test_update_menu_attrib_yield_selected_without_body
2634
+ @fcb.stubs(:body).returns(nil)
2635
+ @hd.expects(:initialize_fcb_names).with(@fcb)
2636
+ @hd.update_menu_attrib_yield_selected(@fcb, [:some_message])
2637
+ end
2638
+ end
2639
+
2640
+ class TestHashDelegatorWaitForUserSelectedBlock < Minitest::Test
2641
+ def setup
2642
+ @hd = HashDelegator.new
2643
+ @hd.stubs(:error_handler)
2644
+ end
2645
+
2646
+ def test_wait_for_user_selected_block_with_back_state
2647
+ mock_block_state = Struct.new(:state, :block).new(MenuState::BACK,
2648
+ { dname: 'back_block' })
2649
+ @hd.stubs(:wait_for_user_selection).returns(mock_block_state)
2650
+
2651
+ result = @hd.wait_for_user_selected_block([], ['Block 1', 'Block 2'],
2652
+ nil)
2653
+
2654
+ assert_equal 'back_block',
2655
+ @hd.instance_variable_get(:@delegate_object)[:block_name]
2656
+ assert @hd.instance_variable_get(:@menu_user_clicked_back_link)
2657
+ assert_equal mock_block_state, result
2658
+ end
2659
+
2660
+ def test_wait_for_user_selected_block_with_continue_state
2661
+ mock_block_state = Struct.new(:state, :block).new(
2662
+ MenuState::CONTINUE, { dname: 'continue_block' }
2663
+ )
2664
+ @hd.stubs(:wait_for_user_selection).returns(mock_block_state)
2665
+
2666
+ result = @hd.wait_for_user_selected_block([], ['Block 1', 'Block 2'],
2667
+ nil)
2668
+
2669
+ assert_equal 'continue_block',
2670
+ @hd.instance_variable_get(:@delegate_object)[:block_name]
2671
+ refute @hd.instance_variable_get(:@menu_user_clicked_back_link)
2672
+ assert_equal mock_block_state, result
2673
+ end
2674
+ end
2675
+
2676
+ ####
2677
+ class TestHashDelegatorYieldToBlock < Minitest::Test
2678
+ def setup
2679
+ @hd = HashDelegator.new
2680
+ @fcb = mock('Fcb')
2681
+ MarkdownExec::Filter.stubs(:fcb_select?).returns(true)
2682
+ end
2683
+
2684
+ def test_yield_to_block_if_applicable_with_correct_conditions
2685
+ block_called = false
2686
+ @hd.yield_to_block_if_applicable(@fcb, [:blocks]) do |type, fcb|
2687
+ block_called = true
2688
+ assert_equal :blocks, type
2689
+ assert_equal @fcb, fcb
2690
+ end
2691
+ assert block_called
2692
+ end
2693
+
2694
+ def test_yield_to_block_if_applicable_without_block
2695
+ result = @hd.yield_to_block_if_applicable(@fcb, [:blocks])
2696
+ assert_nil result
2697
+ end
2698
+
2699
+ def test_yield_to_block_if_applicable_with_incorrect_conditions
2700
+ block_called = false
2701
+ MarkdownExec::Filter.stubs(:fcb_select?).returns(false)
2702
+ @hd.yield_to_block_if_applicable(@fcb, [:non_blocks]) do |_|
2703
+ block_called = true
2704
+ end
2705
+ refute block_called
2706
+ end
2707
+ end
2708
+ end # module MarkdownExec
2709
+ end