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