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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/Gemfile.lock +1 -1
- data/Rakefile +1 -0
- data/bin/tab_completion.sh +2 -2
- data/examples/import0.md +41 -5
- data/examples/import1.md +9 -8
- data/examples/linked1.md +8 -4
- data/lib/array.rb +27 -0
- data/lib/array_util.rb +21 -0
- data/lib/cached_nested_file_reader.rb +51 -31
- data/lib/constants.rb +46 -0
- data/lib/env.rb +2 -1
- data/lib/exceptions.rb +34 -0
- data/lib/fcb.rb +41 -1
- data/lib/filter.rb +32 -17
- data/lib/fout.rb +52 -0
- data/lib/hash.rb +21 -0
- data/lib/hash_delegator.rb +2709 -0
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/markdown_exec.rb +137 -1458
- data/lib/mdoc.rb +0 -3
- data/lib/menu.src.yml +137 -27
- data/lib/menu.yml +131 -23
- data/lib/method_sorter.rb +19 -17
- data/lib/object_present.rb +1 -1
- data/lib/option_value.rb +4 -2
- data/lib/pty1.rb +16 -16
- data/lib/regexp.rb +4 -5
- data/lib/saved_assets.rb +4 -2
- data/lib/saved_files_matcher.rb +7 -3
- data/lib/shared.rb +0 -5
- data/lib/string_util.rb +22 -0
- data/lib/tap.rb +5 -2
- metadata +10 -3
- data/lib/environment_opt_parse.rb +0 -209
@@ -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
|