markdown_exec 1.6 → 1.8
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 +5 -2
- data/Gemfile.lock +1 -1
- data/Rakefile +2 -0
- data/bin/tab_completion.sh +19 -3
- data/examples/colors.md +48 -0
- data/examples/import0.md +41 -5
- data/examples/import1.md +9 -8
- data/examples/include.md +11 -4
- data/examples/linked1.md +8 -4
- data/examples/opts.md +7 -0
- data/lib/ansi_formatter.rb +161 -0
- 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/directory_searcher.rb +239 -0
- 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 +2796 -0
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/markdown_exec.rb +190 -1474
- data/lib/mdoc.rb +148 -36
- data/lib/menu.src.yml +224 -37
- data/lib/menu.yml +209 -33
- data/lib/method_sorter.rb +19 -17
- data/lib/object_present.rb +1 -1
- data/lib/pty1.rb +16 -16
- data/lib/regexp.rb +2 -4
- data/lib/shared.rb +0 -5
- data/lib/string_util.rb +22 -0
- metadata +13 -3
- data/lib/environment_opt_parse.rb +0 -209
data/lib/markdown_exec.rb
CHANGED
@@ -13,13 +13,18 @@ require 'tmpdir'
|
|
13
13
|
require 'tty-prompt'
|
14
14
|
require 'yaml'
|
15
15
|
|
16
|
+
require_relative 'ansi_formatter'
|
16
17
|
require_relative 'block_label'
|
17
18
|
require_relative 'cached_nested_file_reader'
|
18
19
|
require_relative 'cli'
|
19
20
|
require_relative 'colorize'
|
21
|
+
require_relative 'directory_searcher'
|
20
22
|
require_relative 'env'
|
23
|
+
require_relative 'exceptions'
|
21
24
|
require_relative 'fcb'
|
22
25
|
require_relative 'filter'
|
26
|
+
require_relative 'fout'
|
27
|
+
require_relative 'hash_delegator'
|
23
28
|
require_relative 'markdown_exec/version'
|
24
29
|
require_relative 'mdoc'
|
25
30
|
require_relative 'option_value'
|
@@ -42,79 +47,20 @@ MDE_HISTORY_ENV_NAME = 'MDE_MENU_HISTORY'
|
|
42
47
|
#
|
43
48
|
class FileMissingError < StandardError; end
|
44
49
|
|
45
|
-
|
46
|
-
#
|
47
|
-
#
|
48
|
-
class Hash
|
49
|
-
unless defined?(sort_by_key)
|
50
|
-
def sort_by_key
|
51
|
-
keys.sort.to_h { |key| [key, self[key]] }
|
52
|
-
end
|
53
|
-
end
|
54
|
-
|
55
|
-
unless defined?(sym_keys)
|
56
|
-
def sym_keys
|
57
|
-
transform_keys(&:to_sym)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
class LoadFile
|
63
|
-
Load = true
|
64
|
-
Reuse = false
|
65
|
-
end
|
66
|
-
|
67
|
-
class MenuState
|
68
|
-
BACK = :back
|
69
|
-
CONTINUE = :continue
|
70
|
-
EXIT = :exit
|
71
|
-
end
|
72
|
-
|
73
|
-
# integer value for comparison
|
74
|
-
#
|
75
|
-
def options_fetch_display_level(options)
|
76
|
-
options.fetch(:display_level, 1)
|
77
|
-
end
|
78
|
-
|
79
|
-
# integer value for comparison
|
80
|
-
#
|
81
|
-
def options_fetch_display_level_xbase_prefix(options)
|
82
|
-
options.fetch(:level_xbase_prefix, '')
|
50
|
+
def dp(str)
|
51
|
+
lout " => #{str}", level: DISPLAY_LEVEL_DEBUG
|
83
52
|
end
|
84
53
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
#
|
90
|
-
def fout(str)
|
91
|
-
puts str
|
92
|
-
end
|
93
|
-
|
94
|
-
def fout_list(str)
|
95
|
-
puts str
|
96
|
-
end
|
97
|
-
|
98
|
-
def fout_section(name, data)
|
99
|
-
puts "# #{name}"
|
100
|
-
puts data.to_yaml
|
101
|
-
end
|
102
|
-
|
103
|
-
def approved_fout?(level)
|
104
|
-
level <= options_fetch_display_level(@options)
|
105
|
-
end
|
106
|
-
|
107
|
-
# display output at level or lower than filter (DISPLAY_LEVEL_DEFAULT)
|
108
|
-
#
|
109
|
-
def lout(str, level: DISPLAY_LEVEL_BASE)
|
110
|
-
return unless approved_fout? level
|
111
|
-
|
112
|
-
fout level == DISPLAY_LEVEL_BASE ? str : options_fetch_display_level_xbase_prefix(@options) + str
|
113
|
-
end
|
54
|
+
def rbp
|
55
|
+
rpry
|
56
|
+
pp(caller.take(4).map.with_index { |line, ind| " - #{ind}: #{line}" })
|
57
|
+
binding.pry
|
114
58
|
end
|
115
59
|
|
116
|
-
def
|
117
|
-
|
60
|
+
def bpp(*args)
|
61
|
+
pp '+ bpp()'
|
62
|
+
pp(*args.map.with_index { |line, ind| " - #{ind}: #{line}" })
|
63
|
+
rbp
|
118
64
|
end
|
119
65
|
|
120
66
|
def rpry
|
@@ -124,6 +70,13 @@ end
|
|
124
70
|
|
125
71
|
public
|
126
72
|
|
73
|
+
# convert regex match groups to a hash with symbol keys
|
74
|
+
#
|
75
|
+
# :reek:UtilityFunction
|
76
|
+
def extract_named_captures_from_option(str, option)
|
77
|
+
str.match(Regexp.new(option))&.named_captures&.sym_keys
|
78
|
+
end
|
79
|
+
|
127
80
|
# :reek:UtilityFunction
|
128
81
|
def list_recent_output(saved_stdout_folder, saved_stdout_glob,
|
129
82
|
list_count)
|
@@ -138,61 +91,10 @@ def list_recent_scripts(saved_script_folder, saved_script_glob,
|
|
138
91
|
saved_script_glob, list_count)
|
139
92
|
end
|
140
93
|
|
141
|
-
# convert regex match groups to a hash with symbol keys
|
142
|
-
#
|
143
|
-
# :reek:UtilityFunction
|
144
|
-
def extract_named_captures_from_option(str, option)
|
145
|
-
str.match(Regexp.new(option))&.named_captures&.sym_keys
|
146
|
-
end
|
147
|
-
|
148
|
-
module ArrayUtil
|
149
|
-
def self.partition_by_predicate(arr)
|
150
|
-
true_list = []
|
151
|
-
false_list = []
|
152
|
-
|
153
|
-
arr.each do |element|
|
154
|
-
if yield(element)
|
155
|
-
true_list << element
|
156
|
-
else
|
157
|
-
false_list << element
|
158
|
-
end
|
159
|
-
end
|
160
|
-
|
161
|
-
[true_list, false_list]
|
162
|
-
end
|
163
|
-
end
|
164
|
-
|
165
|
-
module StringUtil
|
166
|
-
# Splits the given string on the first occurrence of the specified character.
|
167
|
-
# Returns an array containing the portion of the string before the character and the rest of the string.
|
168
|
-
#
|
169
|
-
# @param input_str [String] The string to be split.
|
170
|
-
# @param split_char [String] The character on which to split the string.
|
171
|
-
# @return [Array<String>] An array containing two elements: the part of the string before split_char, and the rest of the string.
|
172
|
-
def self.partition_at_first(input_str, split_char)
|
173
|
-
split_index = input_str.index(split_char)
|
174
|
-
|
175
|
-
if split_index.nil?
|
176
|
-
[input_str, '']
|
177
|
-
else
|
178
|
-
[input_str[0...split_index], input_str[(split_index + 1)..-1]]
|
179
|
-
end
|
180
|
-
end
|
181
|
-
end
|
182
|
-
|
183
94
|
# execute markdown documents
|
184
95
|
#
|
185
96
|
module MarkdownExec
|
186
|
-
|
187
|
-
FNR11 = '/'
|
188
|
-
FNR12 = ',~'
|
189
|
-
|
190
|
-
SHELL_COLOR_OPTIONS = {
|
191
|
-
BlockType::BASH => :menu_bash_color,
|
192
|
-
BlockType::LINK => :menu_link_color,
|
193
|
-
BlockType::OPTS => :menu_opts_color,
|
194
|
-
BlockType::VARS => :menu_vars_color
|
195
|
-
}.freeze
|
97
|
+
include Exceptions
|
196
98
|
|
197
99
|
##
|
198
100
|
#
|
@@ -203,124 +105,32 @@ module MarkdownExec
|
|
203
105
|
# :reek:TooManyInstanceVariables ### temp
|
204
106
|
# :reek:TooManyMethods ### temp
|
205
107
|
class MarkParse
|
206
|
-
attr_reader :options
|
108
|
+
attr_reader :options, :prompt, :run_state
|
207
109
|
|
208
110
|
include ArrayUtil
|
209
111
|
include StringUtil
|
210
|
-
include FOUT
|
211
112
|
|
212
113
|
def initialize(options = {})
|
213
|
-
@execute_aborted_at = nil
|
214
|
-
@execute_completed_at = nil
|
215
|
-
@execute_error = nil
|
216
|
-
@execute_error_message = nil
|
217
|
-
@execute_files = nil
|
218
|
-
@execute_options = nil
|
219
|
-
@execute_script_filespec = nil
|
220
|
-
@execute_started_at = nil
|
221
114
|
@option_parser = nil
|
222
|
-
@options = options
|
223
|
-
@prompt = tty_prompt_without_disabled_symbol
|
224
|
-
end
|
225
115
|
|
226
|
-
|
227
|
-
|
228
|
-
# @param blocks_in_file [Array] The current blocks in the menu
|
229
|
-
def add_menu_chrome_blocks!(blocks_in_file)
|
230
|
-
return unless @options[:menu_link_format].present?
|
231
|
-
|
232
|
-
if @options[:menu_with_back] && history_state_exist?
|
233
|
-
append_chrome_block(blocks_in_file, MenuState::BACK)
|
234
|
-
end
|
235
|
-
if @options[:menu_with_exit]
|
236
|
-
append_chrome_block(blocks_in_file, MenuState::EXIT)
|
237
|
-
end
|
238
|
-
append_divider(blocks_in_file, @options, :initial)
|
239
|
-
append_divider(blocks_in_file, @options, :final)
|
240
|
-
end
|
241
|
-
|
242
|
-
##
|
243
|
-
# Appends a summary of a block (FCB) to the blocks array.
|
244
|
-
#
|
245
|
-
def append_block_summary(blocks, fcb, opts)
|
246
|
-
## enhance fcb with block summary
|
247
|
-
#
|
248
|
-
blocks.push get_block_summary(opts, fcb)
|
116
|
+
@options = HashDelegator.new(options)
|
117
|
+
@fout = FOut.new(@delegate_object)
|
249
118
|
end
|
250
119
|
|
251
|
-
|
252
|
-
#
|
253
|
-
# @param blocks_in_file [Array] The current blocks in the menu
|
254
|
-
# @param type [Symbol] The type of chrome block to add (:back or :exit)
|
255
|
-
def append_chrome_block(blocks_in_file, type)
|
256
|
-
case type
|
257
|
-
when MenuState::BACK
|
258
|
-
state = history_state_partition(@options)
|
259
|
-
@hs_curr = state[:unit]
|
260
|
-
@hs_rest = state[:rest]
|
261
|
-
option_name = @options[:menu_option_back_name]
|
262
|
-
insert_at_top = @options[:menu_back_at_top]
|
263
|
-
when MenuState::EXIT
|
264
|
-
option_name = @options[:menu_option_exit_name]
|
265
|
-
insert_at_top = @options[:menu_exit_at_top]
|
266
|
-
end
|
120
|
+
private
|
267
121
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
dname: formatted_name.send(@options[:menu_link_color].to_sym),
|
273
|
-
oname: formatted_name
|
122
|
+
def error_handler(name = '', opts = {})
|
123
|
+
Exceptions.error_handler(
|
124
|
+
"CachedNestedFileReader.#{name} -- #{$!}",
|
125
|
+
opts
|
274
126
|
)
|
275
|
-
|
276
|
-
if insert_at_top
|
277
|
-
blocks_in_file.unshift(chrome_block)
|
278
|
-
else
|
279
|
-
blocks_in_file.push(chrome_block)
|
280
|
-
end
|
281
127
|
end
|
282
128
|
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
def append_divider(blocks, opts, position)
|
288
|
-
divider_key = position == :initial ? :menu_initial_divider : :menu_final_divider
|
289
|
-
unless opts[:menu_divider_format].present? && opts[divider_key].present?
|
290
|
-
return
|
291
|
-
end
|
292
|
-
|
293
|
-
oname = format(opts[:menu_divider_format],
|
294
|
-
safeval(opts[divider_key]))
|
295
|
-
divider = FCB.new(
|
296
|
-
chrome: true,
|
297
|
-
disabled: '',
|
298
|
-
dname: oname.send(opts[:menu_divider_color].to_sym),
|
299
|
-
oname: oname
|
129
|
+
def warn_format(name, message, opts = {})
|
130
|
+
Exceptions.warn_format(
|
131
|
+
"CachedNestedFileReader.#{name} -- #{message}",
|
132
|
+
opts
|
300
133
|
)
|
301
|
-
|
302
|
-
position == :initial ? blocks.unshift(divider) : blocks.push(divider)
|
303
|
-
end
|
304
|
-
|
305
|
-
# Execute a code block after approval and provide user interaction options.
|
306
|
-
#
|
307
|
-
# This method displays required code blocks, asks for user approval, and
|
308
|
-
# executes the code block if approved. It also allows users to copy the
|
309
|
-
# code to the clipboard or save it to a file.
|
310
|
-
#
|
311
|
-
# @param opts [Hash] Options hash containing configuration settings.
|
312
|
-
# @param mdoc [YourMDocClass] An instance of the MDoc class.
|
313
|
-
#
|
314
|
-
def approve_and_execute_block(selected, opts, mdoc)
|
315
|
-
if selected.fetch(:shell, '') == BlockType::LINK
|
316
|
-
handle_shell_link(opts, selected.fetch(:body, ''), mdoc)
|
317
|
-
elsif opts.fetch(:s_back, false)
|
318
|
-
handle_back_link(opts)
|
319
|
-
elsif selected[:shell] == BlockType::OPTS
|
320
|
-
handle_shell_opts(opts, selected)
|
321
|
-
else
|
322
|
-
handle_remainder_blocks(mdoc, opts, selected)
|
323
|
-
end
|
324
134
|
end
|
325
135
|
|
326
136
|
# return arguments before `--`
|
@@ -355,249 +165,34 @@ module MarkdownExec
|
|
355
165
|
end.compact.to_h
|
356
166
|
end
|
357
167
|
|
358
|
-
# Finds the first hash-like element within an enumerable collection where the specified key
|
359
|
-
# matches the given value. Returns a default value if no match is found.
|
360
|
-
#
|
361
|
-
# @param blocks [Enumerable] An enumerable collection of hash-like objects.
|
362
|
-
# @param key [Object] The key to look up in each hash-like object.
|
363
|
-
# @param value [Object] The value to compare against the value associated with the key.
|
364
|
-
# @param default [Object] The default value to return if no match is found (optional).
|
365
|
-
# @return [Object, nil] The found hash-like object, or the default value if no match is found.
|
366
|
-
def block_find(blocks, key, value, default = nil)
|
367
|
-
blocks.find { |item| item[key] == value } || default
|
368
|
-
end
|
369
|
-
|
370
|
-
def blocks_per_opts(blocks, opts)
|
371
|
-
return blocks if opts[:struct]
|
372
|
-
|
373
|
-
blocks.map do |block|
|
374
|
-
block.fetch(:text, nil) || block.oname
|
375
|
-
end.compact.reject(&:empty?)
|
376
|
-
end
|
377
|
-
|
378
168
|
def calculated_options
|
379
169
|
{
|
380
170
|
bash: true, # bash block parsing in get_block_summary()
|
381
|
-
saved_script_filename: nil
|
382
|
-
struct: true # allow get_block_summary()
|
171
|
+
saved_script_filename: nil # calculated
|
383
172
|
}
|
384
173
|
end
|
385
174
|
|
386
|
-
# Check whether the document exists and is readable
|
387
|
-
def check_file_existence(filename)
|
388
|
-
unless filename&.present?
|
389
|
-
fout 'No blocks found.'
|
390
|
-
return false
|
391
|
-
end
|
392
|
-
|
393
|
-
unless File.exist? filename
|
394
|
-
fout 'Document is missing.'
|
395
|
-
return false
|
396
|
-
end
|
397
|
-
true
|
398
|
-
end
|
399
|
-
|
400
175
|
def clear_required_file
|
401
176
|
ENV['MDE_LINK_REQUIRED_FILE'] = ''
|
402
177
|
end
|
403
178
|
|
404
|
-
#
|
405
|
-
#
|
406
|
-
#
|
407
|
-
#
|
408
|
-
#
|
409
|
-
|
410
|
-
# Apply hash in opts block to environment variables
|
411
|
-
if selected[:shell] == BlockType::VARS
|
412
|
-
data = YAML.load(selected[:body].join("\n"))
|
413
|
-
data.each_key do |key|
|
414
|
-
ENV[key] = value = data[key].to_s
|
415
|
-
next unless opts[:menu_vars_set_format].present?
|
416
|
-
|
417
|
-
print format(
|
418
|
-
opts[:menu_vars_set_format],
|
419
|
-
{ key: key,
|
420
|
-
value: value }
|
421
|
-
).send(opts[:menu_vars_set_color].to_sym)
|
422
|
-
end
|
423
|
-
end
|
424
|
-
|
425
|
-
required = mdoc.collect_recursively_required_code(opts[:block_name],
|
426
|
-
opts: opts)
|
427
|
-
read_required_blocks_from_temp_file + required[:code]
|
428
|
-
end
|
429
|
-
|
430
|
-
def cfile
|
431
|
-
@cfile ||= CachedNestedFileReader.new(
|
432
|
-
import_pattern: @options.fetch(:import_pattern)
|
433
|
-
)
|
434
|
-
end
|
179
|
+
# # Deletes a required temporary file specified by an environment variable.
|
180
|
+
# # The function checks if the file exists before attempting to delete it.
|
181
|
+
# # Clears the environment variable after deletion.
|
182
|
+
# #
|
183
|
+
# def delete_required_temp_file
|
184
|
+
# temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
|
435
185
|
|
436
|
-
|
437
|
-
EF_STDERR = :stderr
|
438
|
-
EF_STDIN = :stdin
|
186
|
+
# return if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
|
439
187
|
|
440
|
-
#
|
441
|
-
def command_execute(opts, command, args: [])
|
442
|
-
@execute_files = Hash.new([])
|
443
|
-
@execute_options = opts
|
444
|
-
@execute_started_at = Time.now.utc
|
188
|
+
# FileUtils.rm_f(temp_blocks_file_path)
|
445
189
|
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
end
|
451
|
-
handle_stream(opts, stderr, EF_STDERR) do |line|
|
452
|
-
yield nil, nil, line, exec_thr if block_given?
|
453
|
-
end
|
190
|
+
# clear_required_file
|
191
|
+
# rescue StandardError
|
192
|
+
# error_handler('delete_required_temp_file')
|
193
|
+
# end
|
454
194
|
|
455
|
-
|
456
|
-
stdin.puts(line)
|
457
|
-
yield line, nil, nil, exec_thr if block_given?
|
458
|
-
end
|
459
|
-
|
460
|
-
exec_thr.join
|
461
|
-
sleep 0.1
|
462
|
-
in_thr.kill if in_thr&.alive?
|
463
|
-
end
|
464
|
-
|
465
|
-
@execute_completed_at = Time.now.utc
|
466
|
-
rescue Errno::ENOENT => err
|
467
|
-
#d 'command error ENOENT triggered by missing command in script'
|
468
|
-
@execute_aborted_at = Time.now.utc
|
469
|
-
@execute_error_message = err.message
|
470
|
-
@execute_error = err
|
471
|
-
@execute_files[EF_STDERR] += [@execute_error_message]
|
472
|
-
fout "Error ENOENT: #{err.inspect}"
|
473
|
-
rescue SignalException => err
|
474
|
-
#d 'command SIGTERM triggered by user or system'
|
475
|
-
@execute_aborted_at = Time.now.utc
|
476
|
-
@execute_error_message = 'SIGTERM'
|
477
|
-
@execute_error = err
|
478
|
-
@execute_files[EF_STDERR] += [@execute_error_message]
|
479
|
-
fout "Error ENOENT: #{err.inspect}"
|
480
|
-
end
|
481
|
-
|
482
|
-
def command_or_user_selected_block(blocks_in_file, blocks_menu, default,
|
483
|
-
opts)
|
484
|
-
if opts[:block_name].present?
|
485
|
-
block = blocks_in_file.find do |item|
|
486
|
-
item[:oname] == opts[:block_name]
|
487
|
-
end
|
488
|
-
else
|
489
|
-
block, state = wait_for_user_selected_block(blocks_in_file, blocks_menu, default,
|
490
|
-
opts)
|
491
|
-
end
|
492
|
-
|
493
|
-
[block, state]
|
494
|
-
end
|
495
|
-
|
496
|
-
def copy_to_clipboard(required_lines)
|
497
|
-
text = required_lines.flatten.join($INPUT_RECORD_SEPARATOR)
|
498
|
-
Clipboard.copy(text)
|
499
|
-
fout "Clipboard updated: #{required_lines.count} blocks," \
|
500
|
-
" #{required_lines.flatten.count} lines," \
|
501
|
-
" #{text.length} characters"
|
502
|
-
end
|
503
|
-
|
504
|
-
def count_blocks_in_filename
|
505
|
-
fenced_start_and_end_regex = Regexp.new @options[:fenced_start_and_end_regex]
|
506
|
-
cnt = 0
|
507
|
-
cfile.readlines(@options[:filename]).each do |line|
|
508
|
-
cnt += 1 if line.match(fenced_start_and_end_regex)
|
509
|
-
end
|
510
|
-
cnt / 2
|
511
|
-
end
|
512
|
-
|
513
|
-
##
|
514
|
-
# Creates and adds a formatted block to the blocks array based on the provided match and format options.
|
515
|
-
# @param blocks [Array] The array of blocks to add the new block to.
|
516
|
-
# @param fcb [FCB] The file control block containing the line to match against.
|
517
|
-
# @param match_data [MatchData] The match data containing named captures for formatting.
|
518
|
-
# @param format_option [String] The format string to be used for the new block.
|
519
|
-
# @param color_method [Symbol] The color method to apply to the block's display name.
|
520
|
-
def create_and_add_chrome_block(blocks, _fcb, match_data, format_option,
|
521
|
-
color_method)
|
522
|
-
oname = format(format_option,
|
523
|
-
match_data.named_captures.transform_keys(&:to_sym))
|
524
|
-
blocks.push FCB.new(
|
525
|
-
chrome: true,
|
526
|
-
disabled: '',
|
527
|
-
dname: oname.send(color_method),
|
528
|
-
oname: oname
|
529
|
-
)
|
530
|
-
end
|
531
|
-
|
532
|
-
##
|
533
|
-
# Processes lines within the file and converts them into blocks if they match certain criteria.
|
534
|
-
# @param blocks [Array] The array to append new blocks to.
|
535
|
-
# @param fcb [FCB] The file control block being processed.
|
536
|
-
# @param opts [Hash] Options containing configuration for line processing.
|
537
|
-
# @param use_chrome [Boolean] Indicates if the chrome styling should be applied.
|
538
|
-
def create_and_add_chrome_blocks(blocks, fcb, opts, use_chrome)
|
539
|
-
return unless use_chrome
|
540
|
-
|
541
|
-
match_criteria = [
|
542
|
-
{ match: :menu_task_match, format: :menu_task_format,
|
543
|
-
color: :menu_task_color },
|
544
|
-
{ match: :menu_divider_match, format: :menu_divider_format,
|
545
|
-
color: :menu_divider_color },
|
546
|
-
{ match: :menu_note_match, format: :menu_note_format,
|
547
|
-
color: :menu_note_color }
|
548
|
-
]
|
549
|
-
|
550
|
-
match_criteria.each do |criteria|
|
551
|
-
unless opts[criteria[:match]].present? &&
|
552
|
-
(mbody = fcb.body[0].match opts[criteria[:match]])
|
553
|
-
next
|
554
|
-
end
|
555
|
-
|
556
|
-
create_and_add_chrome_block(blocks, fcb, mbody, opts[criteria[:format]],
|
557
|
-
opts[criteria[:color]].to_sym)
|
558
|
-
break
|
559
|
-
end
|
560
|
-
end
|
561
|
-
|
562
|
-
def create_and_write_file_with_permissions(file_path, content,
|
563
|
-
chmod_value)
|
564
|
-
dirname = File.dirname(file_path)
|
565
|
-
FileUtils.mkdir_p dirname
|
566
|
-
File.write(file_path, content)
|
567
|
-
return if chmod_value.zero?
|
568
|
-
|
569
|
-
File.chmod chmod_value, file_path
|
570
|
-
end
|
571
|
-
|
572
|
-
# Deletes a required temporary file specified by an environment variable.
|
573
|
-
# The function checks if the file exists before attempting to delete it.
|
574
|
-
# Clears the environment variable after deletion.
|
575
|
-
#
|
576
|
-
def delete_required_temp_file
|
577
|
-
temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
|
578
|
-
|
579
|
-
if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
|
580
|
-
return
|
581
|
-
end
|
582
|
-
|
583
|
-
FileUtils.rm_f(temp_blocks_file_path)
|
584
|
-
|
585
|
-
clear_required_file
|
586
|
-
end
|
587
|
-
|
588
|
-
# Derives a title from the body of an FCB object.
|
589
|
-
# @param fcb [Object] The FCB object whose title is to be derived.
|
590
|
-
# @return [String] The derived title.
|
591
|
-
def derive_title_from_body(fcb)
|
592
|
-
body_content = fcb&.body
|
593
|
-
return '' unless body_content
|
594
|
-
|
595
|
-
if body_content.count == 1
|
596
|
-
body_content.first
|
597
|
-
else
|
598
|
-
format_multiline_body_as_title(body_content)
|
599
|
-
end
|
600
|
-
end
|
195
|
+
public
|
601
196
|
|
602
197
|
## Determines the correct filename to use for searching files
|
603
198
|
#
|
@@ -606,8 +201,7 @@ module MarkdownExec
|
|
606
201
|
if specified_filename&.present?
|
607
202
|
return specified_filename if specified_filename.start_with?('/')
|
608
203
|
|
609
|
-
File.join(specified_folder || default_folder,
|
610
|
-
specified_filename)
|
204
|
+
File.join(specified_folder || default_folder, specified_filename)
|
611
205
|
elsif specified_folder&.present?
|
612
206
|
File.join(specified_folder,
|
613
207
|
filetree ? @options[:md_filename_match] : @options[:md_filename_glob])
|
@@ -616,45 +210,31 @@ module MarkdownExec
|
|
616
210
|
end
|
617
211
|
end
|
618
212
|
|
619
|
-
|
620
|
-
def display_required_code(opts, required_lines)
|
621
|
-
frame = opts[:output_divider].send(opts[:output_divider_color].to_sym)
|
622
|
-
fout frame
|
623
|
-
required_lines.each { |cb| fout cb }
|
624
|
-
fout frame
|
625
|
-
end
|
213
|
+
private
|
626
214
|
|
627
|
-
def
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
)
|
634
|
-
initialize_and_save_execution_output
|
635
|
-
output_execution_summary
|
636
|
-
output_execution_result
|
637
|
-
end
|
215
|
+
# def error_handler(name = '', event = nil, backtrace = nil)
|
216
|
+
# warn(error = "\n * ERROR * #{name}; #{$!.inspect}")
|
217
|
+
# warn($@.take(4).map.with_index { |line, ind| " * #{ind}: #{line}" })
|
218
|
+
# binding.pry if $tap_enable
|
219
|
+
# raise ArgumentError, error
|
220
|
+
# end
|
638
221
|
|
639
222
|
# Reports and executes block logic
|
640
223
|
def execute_block_logic(files)
|
641
224
|
@options[:filename] = select_document_if_multiple(files)
|
642
|
-
select_approve_and_execute_block
|
643
|
-
struct: true })
|
225
|
+
@options.select_approve_and_execute_block
|
644
226
|
end
|
645
227
|
|
646
228
|
## Executes the block specified in the options
|
647
229
|
#
|
648
|
-
def execute_block_with_error_handling
|
649
|
-
finalize_cli_argument_processing
|
650
|
-
@options[:
|
230
|
+
def execute_block_with_error_handling
|
231
|
+
finalize_cli_argument_processing
|
232
|
+
@options[:input_cli_rest] = @rest
|
651
233
|
execute_code_block_based_on_options(@options)
|
652
|
-
rescue FileMissingError
|
653
|
-
|
654
|
-
rescue StandardError
|
655
|
-
|
656
|
-
binding.pry if $tap_enable
|
657
|
-
raise ArgumentError, error
|
234
|
+
rescue FileMissingError
|
235
|
+
warn "File missing: #{$!}"
|
236
|
+
rescue StandardError
|
237
|
+
error_handler('execute_block_with_error_handling')
|
658
238
|
end
|
659
239
|
|
660
240
|
# Main method to execute a block based on options and block_name
|
@@ -663,47 +243,41 @@ module MarkdownExec
|
|
663
243
|
update_options(options, over: false)
|
664
244
|
|
665
245
|
simple_commands = {
|
666
|
-
doc_glob: -> { fout options[:md_filename_glob] },
|
667
|
-
list_blocks:
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
end).flatten(1)
|
672
|
-
end,
|
673
|
-
list_default_yaml: -> { fout_list list_default_yaml },
|
674
|
-
list_docs: -> { fout_list files },
|
675
|
-
list_default_env: -> { fout_list list_default_env },
|
246
|
+
doc_glob: -> { @fout.fout options[:md_filename_glob] },
|
247
|
+
list_blocks: -> { list_blocks },
|
248
|
+
list_default_yaml: -> { @fout.fout_list list_default_yaml },
|
249
|
+
list_docs: -> { @fout.fout_list files },
|
250
|
+
list_default_env: -> { @fout.fout_list list_default_env },
|
676
251
|
list_recent_output: lambda {
|
677
|
-
fout_list list_recent_output(
|
252
|
+
@fout.fout_list list_recent_output(
|
678
253
|
@options[:saved_stdout_folder],
|
679
254
|
@options[:saved_stdout_glob], @options[:list_count]
|
680
255
|
)
|
681
256
|
},
|
682
257
|
list_recent_scripts: lambda {
|
683
|
-
fout_list list_recent_scripts(
|
258
|
+
@fout.fout_list list_recent_scripts(
|
684
259
|
options[:saved_script_folder],
|
685
260
|
options[:saved_script_glob], options[:list_count]
|
686
261
|
)
|
687
262
|
},
|
688
|
-
pwd: -> { fout File.expand_path('..', __dir__) },
|
263
|
+
pwd: -> { @fout.fout File.expand_path('..', __dir__) },
|
689
264
|
run_last_script: -> { run_last_script },
|
690
265
|
select_recent_output: -> { select_recent_output },
|
691
266
|
select_recent_script: -> { select_recent_script },
|
692
|
-
tab_completions: -> { fout tab_completions },
|
693
|
-
menu_export: -> { fout menu_export }
|
267
|
+
tab_completions: -> { @fout.fout tab_completions },
|
268
|
+
menu_export: -> { @fout.fout menu_export }
|
694
269
|
}
|
695
270
|
|
696
271
|
return if execute_simple_commands(simple_commands)
|
697
272
|
|
698
|
-
files =
|
273
|
+
files = opts_prepare_file_list(options)
|
699
274
|
execute_block_logic(files)
|
700
275
|
return unless @options[:output_saved_script_filename]
|
701
276
|
|
702
|
-
fout "
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
raise ArgumentError, error
|
277
|
+
@fout.fout "script_block_name: #{@options.run_state.script_block_name}"
|
278
|
+
@fout.fout "s_save_filespec: #{@options.run_state.saved_filespec}"
|
279
|
+
rescue StandardError
|
280
|
+
error_handler('execute_code_block_based_on_options')
|
707
281
|
end
|
708
282
|
|
709
283
|
# Executes command based on the provided option keys
|
@@ -719,7 +293,7 @@ module MarkdownExec
|
|
719
293
|
|
720
294
|
## post-parse options configuration
|
721
295
|
#
|
722
|
-
def finalize_cli_argument_processing(rest)
|
296
|
+
def finalize_cli_argument_processing(rest = @rest)
|
723
297
|
## position 0: file or folder (optional)
|
724
298
|
#
|
725
299
|
if (pos = rest.shift)&.present?
|
@@ -736,187 +310,22 @@ module MarkdownExec
|
|
736
310
|
#
|
737
311
|
block_name = rest.shift
|
738
312
|
@options[:block_name] = block_name if block_name.present?
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
index.zero? ? line : " #{line}"
|
748
|
-
end.join("\n") << "\n"
|
749
|
-
end
|
750
|
-
|
751
|
-
## summarize blocks
|
752
|
-
#
|
753
|
-
def get_block_summary(call_options, fcb)
|
754
|
-
opts = optsmerge call_options
|
755
|
-
# return fcb.body unless opts[:struct]
|
756
|
-
return fcb unless opts[:bash]
|
757
|
-
|
758
|
-
fcb.call = fcb.title.match(Regexp.new(opts[:block_calls_scan]))&.fetch(1, nil)
|
759
|
-
titlexcall = if fcb.call
|
760
|
-
fcb.title.sub("%#{fcb.call}", '')
|
761
|
-
else
|
762
|
-
fcb.title
|
763
|
-
end
|
764
|
-
bm = extract_named_captures_from_option(titlexcall,
|
765
|
-
opts[:block_name_match])
|
766
|
-
fcb.stdin = extract_named_captures_from_option(titlexcall,
|
767
|
-
opts[:block_stdin_scan])
|
768
|
-
fcb.stdout = extract_named_captures_from_option(titlexcall,
|
769
|
-
opts[:block_stdout_scan])
|
770
|
-
|
771
|
-
shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
|
772
|
-
fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall
|
773
|
-
fcb.dname = if shell_color_option && opts[shell_color_option].present?
|
774
|
-
fcb.oname.send(opts[shell_color_option].to_sym)
|
775
|
-
else
|
776
|
-
fcb.oname
|
777
|
-
end
|
778
|
-
fcb
|
779
|
-
end
|
780
|
-
|
781
|
-
# Handles the link-back operation.
|
782
|
-
#
|
783
|
-
# @param opts [Hash] Configuration options hash.
|
784
|
-
# @return [Array<Symbol, String>] A tuple containing a LoadFile flag and an empty string.
|
785
|
-
def handle_back_link(opts)
|
786
|
-
history_state_pop(opts)
|
787
|
-
[LoadFile::Load, '']
|
788
|
-
end
|
789
|
-
|
790
|
-
# Handles the execution and display of remainder blocks from a selected menu item.
|
791
|
-
#
|
792
|
-
# @param mdoc [Object] Document object containing code blocks.
|
793
|
-
# @param opts [Hash] Configuration options hash.
|
794
|
-
# @param selected [Hash] Selected item from the menu.
|
795
|
-
# @return [Array<Symbol, String>] A tuple containing a LoadFile flag and an empty string.
|
796
|
-
# @note The function can prompt the user for approval before executing code if opts[:user_must_approve] is true.
|
797
|
-
def handle_remainder_blocks(mdoc, opts, selected)
|
798
|
-
required_lines = collect_required_code_lines(mdoc, selected,
|
799
|
-
opts: opts)
|
800
|
-
if opts[:output_script] || opts[:user_must_approve]
|
801
|
-
display_required_code(opts, required_lines)
|
802
|
-
end
|
803
|
-
allow = if opts[:user_must_approve]
|
804
|
-
prompt_for_user_approval(opts,
|
805
|
-
required_lines)
|
806
|
-
else
|
807
|
-
true
|
808
|
-
end
|
809
|
-
opts[:s_ir_approve] = allow
|
810
|
-
if opts[:s_ir_approve]
|
811
|
-
execute_approved_block(opts,
|
812
|
-
required_lines)
|
813
|
-
end
|
814
|
-
|
815
|
-
[LoadFile::Reuse, '']
|
816
|
-
end
|
817
|
-
|
818
|
-
# Handles the link-shell operation.
|
819
|
-
#
|
820
|
-
# @param opts [Hash] Configuration options hash.
|
821
|
-
# @param body [Array<String>] The body content.
|
822
|
-
# @param mdoc [Object] Document object containing code blocks.
|
823
|
-
# @return [Array<Symbol, String>] A tuple containing a LoadFile flag and a block name.
|
824
|
-
def handle_shell_link(opts, body, mdoc)
|
825
|
-
data = body.present? ? YAML.load(body.join("\n")) : {}
|
826
|
-
data_file = data.fetch('file', nil)
|
827
|
-
return [LoadFile::Reuse, ''] unless data_file
|
828
|
-
|
829
|
-
history_state_push(mdoc, data_file, opts)
|
830
|
-
|
831
|
-
data.fetch('vars', []).each do |var|
|
832
|
-
ENV[var[0]] = var[1].to_s
|
833
|
-
end
|
834
|
-
|
835
|
-
[LoadFile::Load, data.fetch('block', '')]
|
836
|
-
end
|
837
|
-
|
838
|
-
# Handles options for the shell.
|
839
|
-
#
|
840
|
-
# @param opts [Hash] Configuration options hash.
|
841
|
-
# @param selected [Hash] Selected item from the menu.
|
842
|
-
# @return [Array<Symbol, String>] A tuple containing a LoadFile::Reuse flag and an empty string.
|
843
|
-
def handle_shell_opts(opts, selected, tgt2 = nil)
|
844
|
-
data = YAML.load(selected[:body].join("\n"))
|
845
|
-
data.each_key do |key|
|
846
|
-
opts[key.to_sym] = value = data[key]
|
847
|
-
tgt2[key.to_sym] = value if tgt2
|
848
|
-
next unless opts[:menu_opts_set_format].present?
|
849
|
-
|
850
|
-
print format(
|
851
|
-
opts[:menu_opts_set_format],
|
852
|
-
{ key: key,
|
853
|
-
value: value }
|
854
|
-
).send(opts[:menu_opts_set_color].to_sym)
|
855
|
-
end
|
856
|
-
[LoadFile::Reuse, '']
|
857
|
-
end
|
858
|
-
|
859
|
-
# Handles reading and processing lines from a given IO stream
|
860
|
-
#
|
861
|
-
# @param stream [IO] The IO stream to read from (e.g., stdout, stderr, stdin).
|
862
|
-
# @param file_type [Symbol] The type of file to which the stream corresponds.
|
863
|
-
def handle_stream(opts, stream, file_type, swap: false)
|
864
|
-
Thread.new do
|
865
|
-
until (line = stream.gets).nil?
|
866
|
-
@execute_files[file_type] =
|
867
|
-
@execute_files[file_type] + [line.strip]
|
868
|
-
print line if opts[:output_stdout]
|
869
|
-
yield line if block_given?
|
870
|
-
end
|
871
|
-
rescue IOError
|
872
|
-
#d 'stdout IOError, thread killed, do nothing'
|
873
|
-
end
|
874
|
-
end
|
875
|
-
|
876
|
-
def history_state_exist?
|
877
|
-
history = ENV.fetch(MDE_HISTORY_ENV_NAME, '')
|
878
|
-
history.present? ? history : nil
|
879
|
-
end
|
880
|
-
|
881
|
-
def history_state_partition(opts)
|
882
|
-
unit, rest = StringUtil.partition_at_first(
|
883
|
-
ENV.fetch(MDE_HISTORY_ENV_NAME, ''),
|
884
|
-
opts[:history_document_separator]
|
885
|
-
)
|
886
|
-
{ unit: unit, rest: rest }.tap_inspect
|
887
|
-
end
|
888
|
-
|
889
|
-
def history_state_pop(opts)
|
890
|
-
state = history_state_partition(opts)
|
891
|
-
opts[:filename] = state[:unit]
|
892
|
-
ENV[MDE_HISTORY_ENV_NAME] = state[:rest]
|
893
|
-
delete_required_temp_file
|
894
|
-
end
|
895
|
-
|
896
|
-
def history_state_push(mdoc, data_file, opts)
|
897
|
-
[data_file, opts[:block_name]].tap_inspect 'filename, blockname'
|
898
|
-
new_history = opts[:filename] +
|
899
|
-
opts[:history_document_separator] +
|
900
|
-
ENV.fetch(MDE_HISTORY_ENV_NAME, '')
|
901
|
-
opts[:filename] = data_file
|
902
|
-
write_required_blocks_to_temp_file(mdoc, opts[:block_name], opts)
|
903
|
-
ENV[MDE_HISTORY_ENV_NAME] = new_history
|
904
|
-
end
|
905
|
-
|
906
|
-
# Indents all lines in a given string with a specified indentation string.
|
907
|
-
# @param body [String] A multi-line string to be indented.
|
908
|
-
# @param indent [String] The string used for indentation (default is an empty string).
|
909
|
-
# @return [String] A single string with each line indented as specified.
|
910
|
-
def indent_all_lines(body, indent = nil)
|
911
|
-
return body if !indent.present?
|
912
|
-
|
913
|
-
body.lines.map { |line| indent + line.chomp }.join("\n")
|
313
|
+
rescue FileMissingError
|
314
|
+
warn_format('finalize_cli_argument_processing',
|
315
|
+
"File missing -- #{$!}", { abort: true })
|
316
|
+
# @options[:block_name] = ''
|
317
|
+
# @options[:filename] = ''
|
318
|
+
# exit 1
|
319
|
+
rescue StandardError
|
320
|
+
error_handler('finalize_cli_argument_processing')
|
914
321
|
end
|
915
322
|
|
916
323
|
## Sets up the options and returns the parsed arguments
|
917
324
|
#
|
918
325
|
def initialize_and_parse_cli_options
|
919
|
-
@options = base_options
|
326
|
+
# @options = base_options
|
327
|
+
@options = HashDelegator.new(base_options)
|
328
|
+
|
920
329
|
read_configuration_file!(@options,
|
921
330
|
".#{MarkdownExec::APP_NAME.downcase}.yml")
|
922
331
|
|
@@ -929,63 +338,18 @@ module MarkdownExec
|
|
929
338
|
].join("\n")
|
930
339
|
|
931
340
|
menu_iter do |item|
|
932
|
-
|
341
|
+
opts_menu_option_append opts, @options, item
|
933
342
|
end
|
934
343
|
end
|
935
344
|
@option_parser.load
|
936
345
|
@option_parser.environment
|
937
|
-
|
938
|
-
|
939
|
-
@options
|
346
|
+
@rest = rest = @option_parser.parse!(arguments_for_mde)
|
347
|
+
@options.pass_args = ARGV[rest.count + 1..]
|
348
|
+
@options.merge(@options.run_state.to_h)
|
940
349
|
|
941
350
|
rest
|
942
351
|
end
|
943
352
|
|
944
|
-
def initialize_and_save_execution_output
|
945
|
-
return unless @options[:save_execution_output]
|
946
|
-
|
947
|
-
@options[:logged_stdout_filename] =
|
948
|
-
SavedAsset.stdout_name(blockname: @options[:block_name],
|
949
|
-
filename: File.basename(@options[:filename],
|
950
|
-
'.*'),
|
951
|
-
prefix: @options[:logged_stdout_filename_prefix],
|
952
|
-
time: Time.now.utc)
|
953
|
-
|
954
|
-
@logged_stdout_filespec =
|
955
|
-
@options[:logged_stdout_filespec] =
|
956
|
-
File.join @options[:saved_stdout_folder],
|
957
|
-
@options[:logged_stdout_filename]
|
958
|
-
@logged_stdout_filespec = @options[:logged_stdout_filespec]
|
959
|
-
write_execution_output_to_file
|
960
|
-
end
|
961
|
-
|
962
|
-
# Initializes variables for regex and other states
|
963
|
-
def initialize_state(opts)
|
964
|
-
{
|
965
|
-
fenced_start_and_end_regex: Regexp.new(opts[:fenced_start_and_end_regex]),
|
966
|
-
fenced_start_extended_regex: Regexp.new(opts[:fenced_start_extended_regex]),
|
967
|
-
fcb: FCB.new,
|
968
|
-
in_fenced_block: false,
|
969
|
-
headings: []
|
970
|
-
}
|
971
|
-
end
|
972
|
-
|
973
|
-
# Main function to iterate through blocks in file
|
974
|
-
def iter_blocks_in_file(opts = {}, &block)
|
975
|
-
return unless check_file_existence(opts[:filename])
|
976
|
-
|
977
|
-
state = initialize_state(opts)
|
978
|
-
|
979
|
-
selected_messages = yield :filter
|
980
|
-
|
981
|
-
cfile.readlines(opts[:filename]).each do |line|
|
982
|
-
next unless line
|
983
|
-
|
984
|
-
update_line_and_block_state(line, state, opts, selected_messages,
|
985
|
-
&block)
|
986
|
-
end
|
987
|
-
end
|
988
|
-
|
989
353
|
##
|
990
354
|
# Returns a lambda expression based on the given procname.
|
991
355
|
# @param procname [String] The name of the process to generate a lambda for.
|
@@ -994,25 +358,64 @@ module MarkdownExec
|
|
994
358
|
def lambda_for_procname(procname, options)
|
995
359
|
case procname
|
996
360
|
when 'debug'
|
997
|
-
|
361
|
+
->(value) {
|
998
362
|
tap_config value: value
|
999
363
|
}
|
1000
364
|
when 'exit'
|
1001
365
|
->(_) { exit }
|
366
|
+
|
367
|
+
when 'find'
|
368
|
+
->(value) {
|
369
|
+
# initialize_and_parse_cli_options
|
370
|
+
@fout.fout "Searching in: " \
|
371
|
+
"#{HashDelegator.new(@options).string_send_color(@options[:path], :menu_chrome_color)}"
|
372
|
+
searcher = DirectorySearcher.new(value, [@options[:path]])
|
373
|
+
|
374
|
+
@fout.fout 'In directory names'
|
375
|
+
@fout.fout AnsiFormatter.new(options).format_and_highlight_array(
|
376
|
+
searcher.search_in_directory_names, highlight: [value]
|
377
|
+
)
|
378
|
+
|
379
|
+
@fout.fout 'In file names'
|
380
|
+
@fout.fout AnsiFormatter.new(options).format_and_highlight_array(
|
381
|
+
searcher.search_in_file_names, highlight: [value]
|
382
|
+
).join("\n")
|
383
|
+
|
384
|
+
@fout.fout 'In file contents'
|
385
|
+
hash = searcher.search_in_file_contents
|
386
|
+
hash.each.with_index do |(key, v2), i1|
|
387
|
+
@fout.fout format('- %3.d: %s', i1 + 1, key)
|
388
|
+
@fout.fout AnsiFormatter.new(options).format_and_highlight_array(
|
389
|
+
v2.map { |nl| format('=%4.d: %s', nl.index, nl.line) },
|
390
|
+
highlight: [value]
|
391
|
+
)
|
392
|
+
end
|
393
|
+
exit
|
394
|
+
}
|
395
|
+
|
1002
396
|
when 'help'
|
1003
|
-
|
1004
|
-
fout menu_help
|
397
|
+
->(_) {
|
398
|
+
@fout.fout menu_help
|
399
|
+
exit
|
400
|
+
}
|
401
|
+
# when %w[who what where why how which when whom]
|
402
|
+
when 'how'
|
403
|
+
->(value) {
|
404
|
+
# value = 'color'
|
405
|
+
@fout.fout(list_default_yaml.select { |line| line.include? value })
|
1005
406
|
exit
|
1006
407
|
}
|
1007
408
|
when 'path'
|
1008
|
-
->(value) {
|
409
|
+
->(value) {
|
410
|
+
read_configuration_file!(options, value)
|
411
|
+
}
|
1009
412
|
when 'show_config'
|
1010
|
-
|
413
|
+
->(_) {
|
1011
414
|
finalize_cli_argument_processing(options)
|
1012
|
-
fout options.sort_by_key.to_yaml
|
415
|
+
@fout.fout options.sort_by_key.to_yaml
|
1013
416
|
}
|
1014
417
|
when 'val_as_bool'
|
1015
|
-
|
418
|
+
->(value) {
|
1016
419
|
value.instance_of?(::String) ? (value.chomp != '0') : value
|
1017
420
|
}
|
1018
421
|
when 'val_as_int'
|
@@ -1021,7 +424,7 @@ module MarkdownExec
|
|
1021
424
|
->(value) { value.to_s }
|
1022
425
|
when 'version'
|
1023
426
|
lambda { |_|
|
1024
|
-
fout MarkdownExec::VERSION
|
427
|
+
@fout.fout MarkdownExec::VERSION
|
1025
428
|
exit
|
1026
429
|
}
|
1027
430
|
else
|
@@ -1029,6 +432,8 @@ module MarkdownExec
|
|
1029
432
|
end
|
1030
433
|
end
|
1031
434
|
|
435
|
+
def list_blocks; end
|
436
|
+
|
1032
437
|
def list_default_env
|
1033
438
|
menu_iter do |item|
|
1034
439
|
next unless item[:env_var].present?
|
@@ -1051,16 +456,7 @@ module MarkdownExec
|
|
1051
456
|
end.compact.sort
|
1052
457
|
end
|
1053
458
|
|
1054
|
-
|
1055
|
-
list_files_specified(
|
1056
|
-
determine_filename(
|
1057
|
-
specified_filename: options[:filename]&.present? ? options[:filename] : nil,
|
1058
|
-
specified_folder: options[:path],
|
1059
|
-
default_filename: 'README.md',
|
1060
|
-
default_folder: '.'
|
1061
|
-
)
|
1062
|
-
)
|
1063
|
-
end
|
459
|
+
public
|
1064
460
|
|
1065
461
|
## Searches for files based on the specified or default filenames and folders
|
1066
462
|
#
|
@@ -1077,80 +473,7 @@ module MarkdownExec
|
|
1077
473
|
@options[:md_filename_glob]))
|
1078
474
|
end
|
1079
475
|
|
1080
|
-
|
1081
|
-
#
|
1082
|
-
def list_named_blocks_in_file(call_options = {}, &options_block)
|
1083
|
-
opts = optsmerge call_options, options_block
|
1084
|
-
blocks_per_opts(
|
1085
|
-
menu_from_file(opts.merge(struct: true)).select do |fcb|
|
1086
|
-
Filter.fcb_select?(opts.merge(no_chrome: true), fcb)
|
1087
|
-
end, opts
|
1088
|
-
)
|
1089
|
-
end
|
1090
|
-
|
1091
|
-
# return true if values were modified
|
1092
|
-
# execute block once per filename
|
1093
|
-
#
|
1094
|
-
def load_auto_blocks(opts, blocks_in_file)
|
1095
|
-
return unless opts[:document_load_opts_block_name].present?
|
1096
|
-
return if opts[:s_most_recent_filename] == opts[:filename]
|
1097
|
-
|
1098
|
-
block = block_find(blocks_in_file, :oname,
|
1099
|
-
opts[:document_load_opts_block_name])
|
1100
|
-
return unless block
|
1101
|
-
|
1102
|
-
handle_shell_opts(opts, block, @options)
|
1103
|
-
opts[:s_most_recent_filename] = opts[:filename]
|
1104
|
-
true
|
1105
|
-
end
|
1106
|
-
|
1107
|
-
def mdoc_and_menu_from_file(opts)
|
1108
|
-
menu_blocks = menu_from_file(opts.merge(struct: true))
|
1109
|
-
mdoc = MDoc.new(menu_blocks) do |nopts|
|
1110
|
-
opts.merge!(nopts)
|
1111
|
-
end
|
1112
|
-
[menu_blocks, mdoc]
|
1113
|
-
end
|
1114
|
-
|
1115
|
-
## Handles the file loading and returns the blocks in the file and MDoc instance
|
1116
|
-
#
|
1117
|
-
def mdoc_menu_and_selected_from_file(opts)
|
1118
|
-
blocks_in_file, mdoc = mdoc_and_menu_from_file(opts)
|
1119
|
-
if load_auto_blocks(opts, blocks_in_file)
|
1120
|
-
# recreate menu with new options
|
1121
|
-
#
|
1122
|
-
blocks_in_file, mdoc = mdoc_and_menu_from_file(opts)
|
1123
|
-
end
|
1124
|
-
|
1125
|
-
blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
|
1126
|
-
add_menu_chrome_blocks!(blocks_menu)
|
1127
|
-
[blocks_in_file, blocks_menu, mdoc]
|
1128
|
-
end
|
1129
|
-
|
1130
|
-
def menu_chrome_colored_option(opts,
|
1131
|
-
option_symbol = :menu_option_back_name)
|
1132
|
-
if opts[:menu_chrome_color]
|
1133
|
-
menu_chrome_formatted_option(opts,
|
1134
|
-
option_symbol).send(opts[:menu_chrome_color].to_sym)
|
1135
|
-
else
|
1136
|
-
menu_chrome_formatted_option(opts, option_symbol)
|
1137
|
-
end
|
1138
|
-
end
|
1139
|
-
|
1140
|
-
def menu_chrome_formatted_option(opts,
|
1141
|
-
option_symbol = :menu_option_back_name)
|
1142
|
-
val1 = safeval(opts.fetch(option_symbol, ''))
|
1143
|
-
val1 unless opts[:menu_chrome_format]
|
1144
|
-
|
1145
|
-
format(opts[:menu_chrome_format], val1)
|
1146
|
-
end
|
1147
|
-
|
1148
|
-
def menu_export(data = menu_for_optparse)
|
1149
|
-
data.map do |item|
|
1150
|
-
item.delete(:procname)
|
1151
|
-
item
|
1152
|
-
end.to_yaml
|
1153
|
-
end
|
476
|
+
private
|
1154
477
|
|
1155
478
|
##
|
1156
479
|
# Generates a menu suitable for OptionParser from the menu items defined in YAML format.
|
@@ -1164,37 +487,6 @@ module MarkdownExec
|
|
1164
487
|
end
|
1165
488
|
end
|
1166
489
|
|
1167
|
-
##
|
1168
|
-
# Returns a list of blocks in a given file, including dividers, tasks, and other types of blocks.
|
1169
|
-
# The list can be customized via call_options and options_block.
|
1170
|
-
#
|
1171
|
-
# @param call_options [Hash] Options passed as an argument.
|
1172
|
-
# @param options_block [Proc] Block for dynamic option manipulation.
|
1173
|
-
# @return [Array<FCB>] An array of FCB objects representing the blocks.
|
1174
|
-
#
|
1175
|
-
def menu_from_file(call_options = {},
|
1176
|
-
&options_block)
|
1177
|
-
opts = optsmerge(call_options, options_block)
|
1178
|
-
use_chrome = !opts[:no_chrome]
|
1179
|
-
|
1180
|
-
blocks = []
|
1181
|
-
iter_blocks_in_file(opts) do |btype, fcb|
|
1182
|
-
case btype
|
1183
|
-
when :blocks
|
1184
|
-
append_block_summary(blocks, fcb, opts)
|
1185
|
-
when :filter # what btypes are responded to?
|
1186
|
-
%i[blocks line]
|
1187
|
-
when :line
|
1188
|
-
create_and_add_chrome_blocks(blocks, fcb, opts, use_chrome)
|
1189
|
-
end
|
1190
|
-
end
|
1191
|
-
blocks
|
1192
|
-
rescue StandardError => err
|
1193
|
-
warn(error = "ERROR ** MarkParse.menu_from_file(); #{err.inspect}")
|
1194
|
-
warn(caller[0..4])
|
1195
|
-
raise StandardError, error
|
1196
|
-
end
|
1197
|
-
|
1198
490
|
def menu_help
|
1199
491
|
@option_parser.help
|
1200
492
|
end
|
@@ -1203,10 +495,15 @@ module MarkdownExec
|
|
1203
495
|
data.map(&block)
|
1204
496
|
end
|
1205
497
|
|
1206
|
-
def
|
1207
|
-
|
1208
|
-
|
1209
|
-
|
498
|
+
def menu_export(data = menu_for_optparse)
|
499
|
+
data.map do |item|
|
500
|
+
item.delete(:procname)
|
501
|
+
item
|
502
|
+
end.to_yaml
|
503
|
+
end
|
504
|
+
|
505
|
+
def opts_menu_option_append(opts, options, item)
|
506
|
+
return unless item[:long_name].present? || item[:short_name].present?
|
1210
507
|
|
1211
508
|
opts.on(*[
|
1212
509
|
# - long name
|
@@ -1219,9 +516,7 @@ module MarkdownExec
|
|
1219
516
|
|
1220
517
|
# - description and default
|
1221
518
|
[item[:description],
|
1222
|
-
(if item[:default].present?
|
1223
|
-
"[#{value_for_cli item[:default]}]"
|
1224
|
-
end)].compact.join(' '),
|
519
|
+
("[#{value_for_cli item[:default]}]" if item[:default].present?)].compact.join(' '),
|
1225
520
|
|
1226
521
|
# apply proccode, if present, to value
|
1227
522
|
# save value to options hash if option is named
|
@@ -1234,188 +529,15 @@ module MarkdownExec
|
|
1234
529
|
].compact)
|
1235
530
|
end
|
1236
531
|
|
1237
|
-
def
|
1238
|
-
|
1239
|
-
|
1240
|
-
|
1241
|
-
|
1242
|
-
|
1243
|
-
|
1244
|
-
menu_blocks_with_headings: opts[:menu_blocks_with_headings],
|
1245
|
-
title: fcb[:title],
|
1246
|
-
text: fcb[:text],
|
1247
|
-
body: fcb[:body]
|
1248
|
-
)
|
1249
|
-
end.compact
|
1250
|
-
end
|
1251
|
-
|
1252
|
-
def next_block_name_from_command_line_arguments(opts)
|
1253
|
-
if opts[:s_cli_rest].present?
|
1254
|
-
opts[:block_name] = opts[:s_cli_rest].pop
|
1255
|
-
false # repeat_menu
|
1256
|
-
else
|
1257
|
-
true # repeat_menu
|
1258
|
-
end
|
1259
|
-
end
|
1260
|
-
|
1261
|
-
# :reek:ControlParameter
|
1262
|
-
def optsmerge(call_options = {}, options_block = nil)
|
1263
|
-
class_call_options = @options.merge(call_options || {})
|
1264
|
-
if options_block
|
1265
|
-
options_block.call class_call_options
|
1266
|
-
else
|
1267
|
-
class_call_options
|
1268
|
-
end
|
1269
|
-
end
|
1270
|
-
|
1271
|
-
def output_execution_result
|
1272
|
-
oq = [['Block', @options[:block_name], DISPLAY_LEVEL_ADMIN],
|
1273
|
-
['Command',
|
1274
|
-
[MarkdownExec::BIN_NAME,
|
1275
|
-
@options[:filename],
|
1276
|
-
@options[:block_name]].join(' '),
|
1277
|
-
DISPLAY_LEVEL_ADMIN]]
|
1278
|
-
|
1279
|
-
[['Script', :saved_filespec],
|
1280
|
-
['StdOut', :logged_stdout_filespec]].each do |label, name|
|
1281
|
-
if @options[name]
|
1282
|
-
oq << [label, @options[name],
|
1283
|
-
DISPLAY_LEVEL_ADMIN]
|
1284
|
-
end
|
1285
|
-
end
|
1286
|
-
|
1287
|
-
oq.map do |label, value, level|
|
1288
|
-
lout ["#{label}:".yellow, value.to_s].join(' '), level: level
|
1289
|
-
end
|
1290
|
-
end
|
1291
|
-
|
1292
|
-
def output_execution_summary
|
1293
|
-
return unless @options[:output_execution_summary]
|
1294
|
-
|
1295
|
-
fout_section 'summary', {
|
1296
|
-
execute_aborted_at: @execute_aborted_at,
|
1297
|
-
execute_completed_at: @execute_completed_at,
|
1298
|
-
execute_error: @execute_error,
|
1299
|
-
execute_error_message: @execute_error_message,
|
1300
|
-
execute_files: @execute_files,
|
1301
|
-
execute_options: @execute_options,
|
1302
|
-
execute_started_at: @execute_started_at,
|
1303
|
-
execute_script_filespec: @execute_script_filespec
|
1304
|
-
}
|
1305
|
-
end
|
1306
|
-
|
1307
|
-
# Prepare the blocks menu by adding labels and other necessary details.
|
1308
|
-
#
|
1309
|
-
# @param blocks_in_file [Array<Hash>] The list of blocks from the file.
|
1310
|
-
# @param opts [Hash] The options hash.
|
1311
|
-
# @return [Array<Hash>] The updated blocks menu.
|
1312
|
-
def prepare_blocks_menu(blocks_in_file, opts)
|
1313
|
-
# next if fcb.fetch(:disabled, false)
|
1314
|
-
# next unless fcb.fetch(:name, '').present?
|
1315
|
-
replace_consecutive_blanks(blocks_in_file).map do |fcb|
|
1316
|
-
next if Filter.prepared_not_in_menu?(opts, fcb)
|
1317
|
-
|
1318
|
-
fcb.merge!(
|
1319
|
-
name: indent_all_lines(fcb.dname, fcb.fetch(:indent, nil)),
|
1320
|
-
label: BlockLabel.make(
|
1321
|
-
body: fcb[:body],
|
1322
|
-
filename: opts[:filename],
|
1323
|
-
headings: fcb.fetch(:headings, []),
|
1324
|
-
menu_blocks_with_docname: opts[:menu_blocks_with_docname],
|
1325
|
-
menu_blocks_with_headings: opts[:menu_blocks_with_headings],
|
1326
|
-
text: fcb[:text],
|
1327
|
-
title: fcb[:title]
|
1328
|
-
)
|
532
|
+
def opts_prepare_file_list(options)
|
533
|
+
list_files_specified(
|
534
|
+
determine_filename(
|
535
|
+
specified_filename: options[:filename]&.present? ? options[:filename] : nil,
|
536
|
+
specified_folder: options[:path],
|
537
|
+
default_filename: 'README.md',
|
538
|
+
default_folder: '.'
|
1329
539
|
)
|
1330
|
-
|
1331
|
-
end.compact
|
1332
|
-
end
|
1333
|
-
|
1334
|
-
# Prepares and fetches file listings
|
1335
|
-
def prepare_file_list(options)
|
1336
|
-
list_files_per_options(options)
|
1337
|
-
end
|
1338
|
-
|
1339
|
-
def process_fenced_block(fcb, opts, selected_messages, &block)
|
1340
|
-
fcb.oname = fcb.dname = fcb.title || ''
|
1341
|
-
return unless fcb.body
|
1342
|
-
|
1343
|
-
update_title_from_body(fcb)
|
1344
|
-
|
1345
|
-
if block &&
|
1346
|
-
selected_messages.include?(:blocks) &&
|
1347
|
-
Filter.fcb_select?(opts, fcb)
|
1348
|
-
block.call :blocks, fcb
|
1349
|
-
end
|
1350
|
-
end
|
1351
|
-
|
1352
|
-
def process_line(line, _opts, selected_messages, &block)
|
1353
|
-
return unless block && selected_messages.include?(:line)
|
1354
|
-
|
1355
|
-
# dp 'text outside of fcb'
|
1356
|
-
fcb = FCB.new
|
1357
|
-
fcb.body = [line]
|
1358
|
-
block.call(:line, fcb)
|
1359
|
-
end
|
1360
|
-
|
1361
|
-
class MenuOptions
|
1362
|
-
YES = 1
|
1363
|
-
NO = 2
|
1364
|
-
SCRIPT_TO_CLIPBOARD = 3
|
1365
|
-
SAVE_SCRIPT = 4
|
1366
|
-
end
|
1367
|
-
|
1368
|
-
##
|
1369
|
-
# Presents a menu to the user for approving an action and performs additional tasks based on the selection.
|
1370
|
-
# The function provides options for approval, rejection, copying data to clipboard, or saving data to a file.
|
1371
|
-
#
|
1372
|
-
# @param opts [Hash] A hash containing various options for the menu.
|
1373
|
-
# @param required_lines [Array<String>] Lines of text or code that are subject to user approval.
|
1374
|
-
#
|
1375
|
-
# @option opts [String] :prompt_approve_block Prompt text for the approval menu.
|
1376
|
-
# @option opts [String] :prompt_yes Text for the 'Yes' choice in the menu.
|
1377
|
-
# @option opts [String] :prompt_no Text for the 'No' choice in the menu.
|
1378
|
-
# @option opts [String] :prompt_script_to_clipboard Text for the 'Copy to Clipboard' choice in the menu.
|
1379
|
-
# @option opts [String] :prompt_save_script Text for the 'Save to File' choice in the menu.
|
1380
|
-
#
|
1381
|
-
# @return [Boolean] Returns true if the user approves (selects 'Yes'), false otherwise.
|
1382
|
-
##
|
1383
|
-
def prompt_for_user_approval(opts, required_lines)
|
1384
|
-
# Present a selection menu for user approval.
|
1385
|
-
|
1386
|
-
sel = @prompt.select(opts[:prompt_approve_block],
|
1387
|
-
filter: true) do |menu|
|
1388
|
-
menu.default MenuOptions::YES
|
1389
|
-
menu.choice opts[:prompt_yes], MenuOptions::YES
|
1390
|
-
menu.choice opts[:prompt_no], MenuOptions::NO
|
1391
|
-
menu.choice opts[:prompt_script_to_clipboard],
|
1392
|
-
MenuOptions::SCRIPT_TO_CLIPBOARD
|
1393
|
-
menu.choice opts[:prompt_save_script], MenuOptions::SAVE_SCRIPT
|
1394
|
-
end
|
1395
|
-
|
1396
|
-
if sel == MenuOptions::SCRIPT_TO_CLIPBOARD
|
1397
|
-
copy_to_clipboard(required_lines)
|
1398
|
-
elsif sel == MenuOptions::SAVE_SCRIPT
|
1399
|
-
save_to_file(opts, required_lines)
|
1400
|
-
end
|
1401
|
-
|
1402
|
-
sel == MenuOptions::YES
|
1403
|
-
rescue TTY::Reader::InputInterrupt
|
1404
|
-
exit 1
|
1405
|
-
end
|
1406
|
-
|
1407
|
-
def prompt_select_continue(opts)
|
1408
|
-
sel = @prompt.select(
|
1409
|
-
opts[:prompt_after_bash_exec],
|
1410
|
-
filter: true,
|
1411
|
-
quiet: true
|
1412
|
-
) do |menu|
|
1413
|
-
menu.choice opts[:prompt_yes]
|
1414
|
-
menu.choice opts[:prompt_exit]
|
1415
|
-
end
|
1416
|
-
sel == opts[:prompt_exit] ? MenuState::EXIT : MenuState::CONTINUE
|
1417
|
-
rescue TTY::Reader::InputInterrupt
|
1418
|
-
exit 1
|
540
|
+
)
|
1419
541
|
end
|
1420
542
|
|
1421
543
|
# :reek:UtilityFunction ### temp
|
@@ -1426,53 +548,19 @@ module MarkdownExec
|
|
1426
548
|
.transform_keys(&:to_sym))
|
1427
549
|
end
|
1428
550
|
|
1429
|
-
|
1430
|
-
#
|
1431
|
-
# @return [Array<String>] An array containing the lines read from the temporary file.
|
1432
|
-
# @note Relies on the 'MDE_LINK_REQUIRED_FILE' environment variable to locate the file.
|
1433
|
-
def read_required_blocks_from_temp_file
|
1434
|
-
temp_blocks = []
|
1435
|
-
|
1436
|
-
temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
|
1437
|
-
if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
|
1438
|
-
return temp_blocks
|
1439
|
-
end
|
1440
|
-
|
1441
|
-
if File.exist?(temp_blocks_file_path)
|
1442
|
-
temp_blocks = File.readlines(temp_blocks_file_path, chomp: true)
|
1443
|
-
end
|
1444
|
-
|
1445
|
-
temp_blocks
|
1446
|
-
end
|
1447
|
-
|
1448
|
-
# Replace duplicate blanks (where :oname is not present) with a single blank line.
|
1449
|
-
#
|
1450
|
-
# @param [Array<Hash>] lines Array of hashes to process.
|
1451
|
-
# @return [Array<Hash>] Cleaned array with consecutive blanks collapsed into one.
|
1452
|
-
def replace_consecutive_blanks(lines)
|
1453
|
-
lines.chunk_while do |i, j|
|
1454
|
-
i[:oname].to_s.empty? && j[:oname].to_s.empty?
|
1455
|
-
end.map do |chunk|
|
1456
|
-
if chunk.any? do |line|
|
1457
|
-
line[:oname].to_s.strip.empty?
|
1458
|
-
end
|
1459
|
-
chunk.first
|
1460
|
-
else
|
1461
|
-
chunk
|
1462
|
-
end
|
1463
|
-
end.flatten
|
1464
|
-
end
|
551
|
+
public
|
1465
552
|
|
1466
553
|
def run
|
1467
554
|
clear_required_file
|
1468
|
-
|
1469
|
-
|
1470
|
-
|
1471
|
-
|
1472
|
-
|
1473
|
-
raise ArgumentError, error
|
555
|
+
initialize_and_parse_cli_options
|
556
|
+
execute_block_with_error_handling
|
557
|
+
@options.delete_required_temp_file
|
558
|
+
rescue StandardError
|
559
|
+
error_handler('run')
|
1474
560
|
end
|
1475
561
|
|
562
|
+
private
|
563
|
+
|
1476
564
|
def run_last_script
|
1477
565
|
filename = SavedFilesMatcher.most_recent(@options[:saved_script_folder],
|
1478
566
|
@options[:saved_script_glob])
|
@@ -1480,89 +568,20 @@ module MarkdownExec
|
|
1480
568
|
|
1481
569
|
saved_name_split filename
|
1482
570
|
@options[:save_executed_script] = false
|
1483
|
-
select_approve_and_execute_block
|
1484
|
-
end
|
1485
|
-
|
1486
|
-
def safeval(str)
|
1487
|
-
eval(str)
|
571
|
+
@options.select_approve_and_execute_block
|
1488
572
|
rescue StandardError
|
1489
|
-
|
1490
|
-
binding.pry if $tap_enable
|
1491
|
-
raise StandardError, $!
|
1492
|
-
end
|
1493
|
-
|
1494
|
-
def save_to_file(opts, required_lines)
|
1495
|
-
write_command_file(opts.merge(save_executed_script: true),
|
1496
|
-
required_lines)
|
1497
|
-
fout "File saved: #{@options[:saved_filespec]}"
|
573
|
+
error_handler('run_last_script')
|
1498
574
|
end
|
1499
575
|
|
1500
576
|
def saved_name_split(name)
|
1501
577
|
# rubocop:disable Layout/LineLength
|
1502
|
-
mf = /#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_,_(?<block>.+)\.sh/.match
|
578
|
+
mf = /#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_,_(?<block>.+)\.sh/.match(name)
|
1503
579
|
# rubocop:enable Layout/LineLength
|
1504
580
|
return unless mf
|
1505
581
|
|
1506
582
|
@options[:block_name] = mf[:block]
|
1507
|
-
@options[:filename] = mf[:file].gsub(
|
1508
|
-
|
1509
|
-
|
1510
|
-
# Select and execute a code block from a Markdown document.
|
1511
|
-
#
|
1512
|
-
# This method allows the user to interactively select a code block from a
|
1513
|
-
# Markdown document, obtain approval, and execute the chosen block of code.
|
1514
|
-
#
|
1515
|
-
# @param call_options [Hash] Initial options for the method.
|
1516
|
-
# @param options_block [Block] Block of options to be merged with call_options.
|
1517
|
-
# @return [Nil] Returns nil if no code block is selected or an error occurs.
|
1518
|
-
def select_approve_and_execute_block(call_options = {},
|
1519
|
-
&options_block)
|
1520
|
-
base_opts = optsmerge(call_options, options_block)
|
1521
|
-
repeat_menu = true && !base_opts[:block_name].present?
|
1522
|
-
load_file = LoadFile::Reuse
|
1523
|
-
default = nil
|
1524
|
-
block = nil
|
1525
|
-
|
1526
|
-
loop do
|
1527
|
-
loop do
|
1528
|
-
opts = base_opts.dup
|
1529
|
-
opts[:s_back] = false
|
1530
|
-
blocks_in_file, blocks_menu, mdoc = mdoc_menu_and_selected_from_file(opts)
|
1531
|
-
block, state = command_or_user_selected_block(blocks_in_file, blocks_menu,
|
1532
|
-
default, opts)
|
1533
|
-
return if state == MenuState::EXIT
|
1534
|
-
|
1535
|
-
load_file, next_block_name = approve_and_execute_block(block, opts,
|
1536
|
-
mdoc)
|
1537
|
-
default = load_file == LoadFile::Load ? nil : opts[:block_name]
|
1538
|
-
base_opts[:block_name] = opts[:block_name] = next_block_name
|
1539
|
-
base_opts[:filename] = opts[:filename]
|
1540
|
-
|
1541
|
-
# user prompt to exit if the menu will be displayed again
|
1542
|
-
#
|
1543
|
-
if repeat_menu &&
|
1544
|
-
block[:shell] == BlockType::BASH &&
|
1545
|
-
opts[:pause_after_bash_exec] &&
|
1546
|
-
prompt_select_continue(opts) == MenuState::EXIT
|
1547
|
-
return
|
1548
|
-
end
|
1549
|
-
|
1550
|
-
# exit current document/menu if loading next document or single block_name was specified
|
1551
|
-
#
|
1552
|
-
if state == MenuState::CONTINUE && load_file == LoadFile::Load
|
1553
|
-
break
|
1554
|
-
end
|
1555
|
-
break unless repeat_menu
|
1556
|
-
end
|
1557
|
-
break if load_file == LoadFile::Reuse
|
1558
|
-
|
1559
|
-
repeat_menu = next_block_name_from_command_line_arguments(base_opts)
|
1560
|
-
end
|
1561
|
-
rescue StandardError => err
|
1562
|
-
warn(error = "ERROR ** MarkParse.select_approve_and_execute_block(); #{err.inspect}")
|
1563
|
-
warn err.backtrace
|
1564
|
-
binding.pry if $tap_enable
|
1565
|
-
raise ArgumentError, error
|
583
|
+
@options[:filename] = mf[:file].gsub(@options[:saved_filename_pattern],
|
584
|
+
@options[:saved_filename_replacement])
|
1566
585
|
end
|
1567
586
|
|
1568
587
|
def select_document_if_multiple(files = list_markdown_files_in_path)
|
@@ -1571,44 +590,24 @@ module MarkdownExec
|
|
1571
590
|
return unless count >= 2
|
1572
591
|
|
1573
592
|
opts = options.dup
|
1574
|
-
select_option_or_exit
|
1575
|
-
|
593
|
+
select_option_or_exit(HashDelegator.new(@options).string_send_color(opts[:prompt_select_md].to_s, :prompt_color_after_script_execution),
|
594
|
+
files,
|
595
|
+
opts.merge(per_page: opts[:select_page_height]))
|
1576
596
|
end
|
1577
597
|
|
1578
598
|
# Presents a TTY prompt to select an option or exit, returns selected option or nil
|
1579
|
-
def select_option_or_exit(prompt_text,
|
1580
|
-
result = select_option_with_metadata(prompt_text,
|
599
|
+
def select_option_or_exit(prompt_text, strings, opts = {})
|
600
|
+
result = @options.select_option_with_metadata(prompt_text, strings,
|
601
|
+
opts)
|
1581
602
|
return unless result.fetch(:option, nil)
|
1582
603
|
|
1583
604
|
result[:selected]
|
1584
605
|
end
|
1585
606
|
|
1586
|
-
# Presents a TTY prompt to select an option or exit, returns metadata including option and selected
|
1587
|
-
def select_option_with_metadata(prompt_text, items, opts = {})
|
1588
|
-
selection = @prompt.select(prompt_text,
|
1589
|
-
items,
|
1590
|
-
opts.merge(filter: true))
|
1591
|
-
|
1592
|
-
items.find { |item| item[:dname] == selection }
|
1593
|
-
.merge(
|
1594
|
-
if selection == menu_chrome_colored_option(opts,
|
1595
|
-
:menu_option_back_name)
|
1596
|
-
{ option: selection, curr: @hs_curr, rest: @hs_rest,
|
1597
|
-
shell: BlockType::LINK }
|
1598
|
-
elsif selection == menu_chrome_colored_option(opts,
|
1599
|
-
:menu_option_exit_name)
|
1600
|
-
{ option: selection }
|
1601
|
-
else
|
1602
|
-
{ selected: selection }
|
1603
|
-
end
|
1604
|
-
)
|
1605
|
-
rescue TTY::Reader::InputInterrupt
|
1606
|
-
exit 1
|
1607
|
-
end
|
1608
|
-
|
1609
607
|
def select_recent_output
|
1610
608
|
filename = select_option_or_exit(
|
1611
|
-
@options[:prompt_select_output].to_s,
|
609
|
+
HashDelegator.new(@options).string_send_color(@options[:prompt_select_output].to_s,
|
610
|
+
:prompt_color_after_script_execution),
|
1612
611
|
list_recent_output(
|
1613
612
|
@options[:saved_stdout_folder],
|
1614
613
|
@options[:saved_stdout_glob],
|
@@ -1623,7 +622,8 @@ module MarkdownExec
|
|
1623
622
|
|
1624
623
|
def select_recent_script
|
1625
624
|
filename = select_option_or_exit(
|
1626
|
-
@options[:prompt_select_md].to_s,
|
625
|
+
HashDelegator.new(@options).string_send_color(@options[:prompt_select_md].to_s,
|
626
|
+
:prompt_color_after_script_execution),
|
1627
627
|
list_recent_scripts(
|
1628
628
|
@options[:saved_script_folder],
|
1629
629
|
@options[:saved_script_glob],
|
@@ -1635,38 +635,10 @@ module MarkdownExec
|
|
1635
635
|
|
1636
636
|
saved_name_split(filename)
|
1637
637
|
|
1638
|
-
select_approve_and_execute_block({
|
1639
|
-
save_executed_script: false,
|
1640
|
-
struct: true })
|
638
|
+
@options.select_approve_and_execute_block ### ({ save_executed_script: false })
|
1641
639
|
end
|
1642
640
|
|
1643
|
-
|
1644
|
-
fenced_start_extended_regex)
|
1645
|
-
fcb_title_groups = line.match(fenced_start_extended_regex).named_captures.sym_keys
|
1646
|
-
rest = fcb_title_groups.fetch(:rest, '')
|
1647
|
-
|
1648
|
-
fcb = FCB.new
|
1649
|
-
fcb.headings = headings
|
1650
|
-
fcb.oname = fcb.dname = fcb_title_groups.fetch(:name, '')
|
1651
|
-
fcb.indent = fcb_title_groups.fetch(:indent, '')
|
1652
|
-
fcb.shell = fcb_title_groups.fetch(:shell, '')
|
1653
|
-
fcb.title = fcb_title_groups.fetch(:name, '')
|
1654
|
-
fcb.body = []
|
1655
|
-
fcb.reqs, fcb.wraps =
|
1656
|
-
ArrayUtil.partition_by_predicate(rest.scan(/\+[^\s]+/).map do |req|
|
1657
|
-
req[1..-1]
|
1658
|
-
end) do |name|
|
1659
|
-
!name.match(Regexp.new(opts[:block_name_wrapper_match]))
|
1660
|
-
end
|
1661
|
-
fcb.call = rest.match(Regexp.new(opts[:block_calls_scan]))&.to_a&.first
|
1662
|
-
fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
|
1663
|
-
tn.named_captures.sym_keys
|
1664
|
-
end
|
1665
|
-
fcb.stdout = if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
|
1666
|
-
tn.named_captures.sym_keys
|
1667
|
-
end
|
1668
|
-
fcb
|
1669
|
-
end
|
641
|
+
public
|
1670
642
|
|
1671
643
|
def tab_completions(data = menu_for_optparse)
|
1672
644
|
data.map do |item|
|
@@ -1674,89 +646,6 @@ module MarkdownExec
|
|
1674
646
|
end.compact
|
1675
647
|
end
|
1676
648
|
|
1677
|
-
def tty_prompt_without_disabled_symbol
|
1678
|
-
TTY::Prompt.new(interrupt: lambda {
|
1679
|
-
puts;
|
1680
|
-
raise TTY::Reader::InputInterrupt
|
1681
|
-
},
|
1682
|
-
symbols: { cross: ' ' })
|
1683
|
-
end
|
1684
|
-
|
1685
|
-
##
|
1686
|
-
# Updates the hierarchy of document headings based on the given line and existing headings.
|
1687
|
-
# The function uses regular expressions specified in the `opts` to identify different levels of headings.
|
1688
|
-
#
|
1689
|
-
# @param line [String] The line of text to examine for heading content.
|
1690
|
-
# @param headings [Array<String>] The existing list of document headings.
|
1691
|
-
# @param opts [Hash] A hash containing options for regular expression matches for different heading levels.
|
1692
|
-
#
|
1693
|
-
# @option opts [String] :heading1_match Regular expression for matching first-level headings.
|
1694
|
-
# @option opts [String] :heading2_match Regular expression for matching second-level headings.
|
1695
|
-
# @option opts [String] :heading3_match Regular expression for matching third-level headings.
|
1696
|
-
#
|
1697
|
-
# @return [Array<String>] Updated list of headings.
|
1698
|
-
def update_document_headings(line, headings, opts)
|
1699
|
-
if (lm = line.match(Regexp.new(opts[:heading3_match])))
|
1700
|
-
[headings[0], headings[1], lm[:name]]
|
1701
|
-
elsif (lm = line.match(Regexp.new(opts[:heading2_match])))
|
1702
|
-
[headings[0], lm[:name]]
|
1703
|
-
elsif (lm = line.match(Regexp.new(opts[:heading1_match])))
|
1704
|
-
[lm[:name]]
|
1705
|
-
else
|
1706
|
-
headings
|
1707
|
-
end
|
1708
|
-
end
|
1709
|
-
|
1710
|
-
##
|
1711
|
-
# Processes an individual line within a loop, updating headings and handling fenced code blocks.
|
1712
|
-
# This function is designed to be called within a loop that iterates through each line of a document.
|
1713
|
-
#
|
1714
|
-
# @param line [String] The current line being processed.
|
1715
|
-
# @param state [Hash] The current state of the parser, including flags and data related to the processing.
|
1716
|
-
# @param opts [Hash] A hash containing various options for line and block processing.
|
1717
|
-
# @param selected_messages [Array<String>] Accumulator for lines or messages that are subject to further processing.
|
1718
|
-
# @param block [Proc] An optional block for further processing or transformation of lines.
|
1719
|
-
#
|
1720
|
-
# @option state [Array<String>] :headings Current headings to be updated based on the line.
|
1721
|
-
# @option state [Regexp] :fenced_start_and_end_regex Regular expression to match the start and end of a fenced block.
|
1722
|
-
# @option state [Boolean] :in_fenced_block Flag indicating whether the current line is inside a fenced block.
|
1723
|
-
# @option state [Object] :fcb An object representing the current fenced code block being processed.
|
1724
|
-
#
|
1725
|
-
# @option opts [Boolean] :menu_blocks_with_headings Flag indicating whether to update headings while processing.
|
1726
|
-
#
|
1727
|
-
# @return [Void] The function modifies the `state` and `selected_messages` arguments in place.
|
1728
|
-
##
|
1729
|
-
def update_line_and_block_state(line, state, opts, selected_messages,
|
1730
|
-
&block)
|
1731
|
-
if opts[:menu_blocks_with_headings]
|
1732
|
-
state[:headings] =
|
1733
|
-
update_document_headings(line, state[:headings], opts)
|
1734
|
-
end
|
1735
|
-
|
1736
|
-
if line.match(state[:fenced_start_and_end_regex])
|
1737
|
-
if state[:in_fenced_block]
|
1738
|
-
process_fenced_block(state[:fcb], opts, selected_messages,
|
1739
|
-
&block)
|
1740
|
-
state[:in_fenced_block] = false
|
1741
|
-
else
|
1742
|
-
state[:fcb] =
|
1743
|
-
start_fenced_block(opts, line, state[:headings],
|
1744
|
-
state[:fenced_start_extended_regex])
|
1745
|
-
state[:in_fenced_block] = true
|
1746
|
-
end
|
1747
|
-
elsif state[:in_fenced_block] && state[:fcb].body
|
1748
|
-
## add line to fenced code block
|
1749
|
-
# remove fcb indent if possible
|
1750
|
-
#
|
1751
|
-
state[:fcb].body += [
|
1752
|
-
line.chomp.sub(/^#{state[:fcb].indent}/, '')
|
1753
|
-
]
|
1754
|
-
|
1755
|
-
else
|
1756
|
-
process_line(line, opts, selected_messages, &block)
|
1757
|
-
end
|
1758
|
-
end
|
1759
|
-
|
1760
649
|
# :reek:BooleanParameter
|
1761
650
|
# :reek:ControlParameter
|
1762
651
|
def update_options(opts = {}, over: true)
|
@@ -1767,125 +656,6 @@ module MarkdownExec
|
|
1767
656
|
end
|
1768
657
|
@options
|
1769
658
|
end
|
1770
|
-
|
1771
|
-
# Updates the title of an FCB object from its body content if the title is nil or empty.
|
1772
|
-
def update_title_from_body(fcb)
|
1773
|
-
return unless fcb.title.nil? || fcb.title.empty?
|
1774
|
-
|
1775
|
-
fcb.title = derive_title_from_body(fcb)
|
1776
|
-
end
|
1777
|
-
|
1778
|
-
def wait_for_user_selected_block(blocks_in_file, blocks_menu,
|
1779
|
-
default, opts)
|
1780
|
-
block, state = wait_for_user_selection(blocks_in_file, blocks_menu,
|
1781
|
-
default, opts)
|
1782
|
-
case state
|
1783
|
-
when MenuState::BACK
|
1784
|
-
opts[:block_name] = block[:dname]
|
1785
|
-
opts[:s_back] = true
|
1786
|
-
when MenuState::CONTINUE
|
1787
|
-
opts[:block_name] = block[:dname]
|
1788
|
-
end
|
1789
|
-
|
1790
|
-
[block, state]
|
1791
|
-
end
|
1792
|
-
|
1793
|
-
## Handles the menu interaction and returns selected block and option state
|
1794
|
-
#
|
1795
|
-
def wait_for_user_selection(blocks_in_file, blocks_menu, default,
|
1796
|
-
opts)
|
1797
|
-
pt = opts[:prompt_select_block].to_s
|
1798
|
-
bm = prepare_blocks_menu(blocks_menu, opts)
|
1799
|
-
return [nil, MenuState::EXIT] if bm.count.zero?
|
1800
|
-
|
1801
|
-
o2 = if default
|
1802
|
-
opts.merge(default: default)
|
1803
|
-
else
|
1804
|
-
opts
|
1805
|
-
end
|
1806
|
-
|
1807
|
-
obj = select_option_with_metadata(pt, bm, o2.merge(
|
1808
|
-
per_page: opts[:select_page_height]
|
1809
|
-
))
|
1810
|
-
|
1811
|
-
case obj.fetch(:oname, nil)
|
1812
|
-
when menu_chrome_formatted_option(opts, :menu_option_exit_name)
|
1813
|
-
[nil, MenuState::EXIT]
|
1814
|
-
when menu_chrome_formatted_option(opts, :menu_option_back_name)
|
1815
|
-
[obj, MenuState::BACK]
|
1816
|
-
else
|
1817
|
-
[obj, MenuState::CONTINUE]
|
1818
|
-
end
|
1819
|
-
rescue StandardError => err
|
1820
|
-
warn(error = "ERROR ** MarkParse.wait_for_user_selection(); #{err.inspect}")
|
1821
|
-
warn caller.take(3)
|
1822
|
-
binding.pry if $tap_enable
|
1823
|
-
raise ArgumentError, error
|
1824
|
-
end
|
1825
|
-
|
1826
|
-
# Handles the core logic for generating the command file's metadata and content.
|
1827
|
-
def write_command_file(call_options, required_lines)
|
1828
|
-
return unless call_options[:save_executed_script]
|
1829
|
-
|
1830
|
-
time_now = Time.now.utc
|
1831
|
-
opts = optsmerge call_options
|
1832
|
-
opts[:saved_script_filename] =
|
1833
|
-
SavedAsset.script_name(blockname: opts[:block_name],
|
1834
|
-
filename: opts[:filename],
|
1835
|
-
prefix: opts[:saved_script_filename_prefix],
|
1836
|
-
time: time_now)
|
1837
|
-
|
1838
|
-
@execute_script_filespec =
|
1839
|
-
@options[:saved_filespec] =
|
1840
|
-
File.join opts[:saved_script_folder],
|
1841
|
-
opts[:saved_script_filename]
|
1842
|
-
|
1843
|
-
shebang = if @options[:shebang]&.present?
|
1844
|
-
"#{@options[:shebang]} #{@options[:shell]}\n"
|
1845
|
-
else
|
1846
|
-
''
|
1847
|
-
end
|
1848
|
-
|
1849
|
-
content = shebang +
|
1850
|
-
"# file_name: #{opts[:filename]}\n" \
|
1851
|
-
"# block_name: #{opts[:block_name]}\n" \
|
1852
|
-
"# time: #{time_now}\n" \
|
1853
|
-
"#{required_lines.flatten.join("\n")}\n"
|
1854
|
-
|
1855
|
-
create_and_write_file_with_permissions(@options[:saved_filespec], content,
|
1856
|
-
@options[:saved_script_chmod])
|
1857
|
-
end
|
1858
|
-
|
1859
|
-
def write_execution_output_to_file
|
1860
|
-
FileUtils.mkdir_p File.dirname(@options[:logged_stdout_filespec])
|
1861
|
-
|
1862
|
-
ol = ["-STDOUT-\n"]
|
1863
|
-
ol += @execute_files&.fetch(EF_STDOUT, [])
|
1864
|
-
ol += ["\n-STDERR-\n"]
|
1865
|
-
ol += @execute_files&.fetch(EF_STDERR, [])
|
1866
|
-
ol += ["\n-STDIN-\n"]
|
1867
|
-
ol += @execute_files&.fetch(EF_STDIN, [])
|
1868
|
-
ol += ["\n"]
|
1869
|
-
File.write(@options[:logged_stdout_filespec], ol.join)
|
1870
|
-
end
|
1871
|
-
|
1872
|
-
# Writes required code blocks to a temporary file and sets an environment variable with its path.
|
1873
|
-
#
|
1874
|
-
# @param block_name [String] The name of the block to collect code for.
|
1875
|
-
# @param opts [Hash] Additional options for collecting code.
|
1876
|
-
# @note Sets the 'MDE_LINK_REQUIRED_FILE' environment variable to the temporary file path.
|
1877
|
-
def write_required_blocks_to_temp_file(mdoc, block_name, opts = {})
|
1878
|
-
code_blocks = (read_required_blocks_from_temp_file +
|
1879
|
-
mdoc.collect_recursively_required_code(
|
1880
|
-
block_name,
|
1881
|
-
opts: opts
|
1882
|
-
)[:code]).join("\n")
|
1883
|
-
|
1884
|
-
Dir::Tmpname.create(self.class.to_s) do |path|
|
1885
|
-
File.write(path, code_blocks)
|
1886
|
-
ENV['MDE_LINK_REQUIRED_FILE'] = path
|
1887
|
-
end
|
1888
|
-
end
|
1889
659
|
end # class MarkParse
|
1890
660
|
end # module MarkdownExec
|
1891
661
|
|
@@ -1896,60 +666,6 @@ if $PROGRAM_NAME == __FILE__
|
|
1896
666
|
require 'minitest/autorun'
|
1897
667
|
|
1898
668
|
module MarkdownExec
|
1899
|
-
class TestMarkParse < Minitest::Test
|
1900
|
-
require 'mocha/minitest'
|
1901
|
-
|
1902
|
-
def test_calling_execute_approved_block_calls_command_execute_with_argument_args_value
|
1903
|
-
pigeon = 'E'
|
1904
|
-
obj = { s_pass_args: pigeon }
|
1905
|
-
|
1906
|
-
c = MarkdownExec::MarkParse.new
|
1907
|
-
|
1908
|
-
# Expect that method command_execute is called with argument args having value pigeon
|
1909
|
-
c.expects(:command_execute).with(
|
1910
|
-
obj,
|
1911
|
-
'',
|
1912
|
-
args: pigeon
|
1913
|
-
)
|
1914
|
-
|
1915
|
-
# Call method execute_approved_block
|
1916
|
-
c.execute_approved_block(obj, [])
|
1917
|
-
end
|
1918
|
-
|
1919
|
-
def setup
|
1920
|
-
@mark_parse = MarkdownExec::MarkParse.new
|
1921
|
-
end
|
1922
|
-
|
1923
|
-
def test_set_fcb_title
|
1924
|
-
# sample input and output data for testing update_title_from_body method
|
1925
|
-
input_output_data = [
|
1926
|
-
{
|
1927
|
-
input: FCB.new(title: nil, body: ["puts 'Hello, world!'"]),
|
1928
|
-
output: "puts 'Hello, world!'"
|
1929
|
-
},
|
1930
|
-
{
|
1931
|
-
input: FCB.new(title: '',
|
1932
|
-
body: ['def add(x, y)',
|
1933
|
-
' x + y', 'end']),
|
1934
|
-
output: "def add(x, y)\n x + y\n end\n"
|
1935
|
-
},
|
1936
|
-
{
|
1937
|
-
input: FCB.new(title: 'foo', body: %w[bar baz]),
|
1938
|
-
output: 'foo' # expect the title to remain unchanged
|
1939
|
-
}
|
1940
|
-
]
|
1941
|
-
|
1942
|
-
# iterate over the input and output data and
|
1943
|
-
# assert that the method sets the title as expected
|
1944
|
-
input_output_data.each do |data|
|
1945
|
-
input = data[:input]
|
1946
|
-
output = data[:output]
|
1947
|
-
@mark_parse.update_title_from_body(input)
|
1948
|
-
assert_equal output, input.title
|
1949
|
-
end
|
1950
|
-
end
|
1951
|
-
end
|
1952
|
-
|
1953
669
|
def test_select_block
|
1954
670
|
blocks = [block1, block2]
|
1955
671
|
menu = [m1, m2]
|