markdown_exec 1.6 → 1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/Gemfile.lock +1 -1
- data/Rakefile +1 -0
- data/bin/tab_completion.sh +2 -2
- data/examples/import0.md +41 -5
- data/examples/import1.md +9 -8
- data/examples/linked1.md +8 -4
- data/lib/array.rb +27 -0
- data/lib/array_util.rb +21 -0
- data/lib/cached_nested_file_reader.rb +51 -31
- data/lib/constants.rb +46 -0
- data/lib/env.rb +2 -1
- data/lib/exceptions.rb +34 -0
- data/lib/fcb.rb +41 -1
- data/lib/filter.rb +32 -17
- data/lib/fout.rb +52 -0
- data/lib/hash.rb +21 -0
- data/lib/hash_delegator.rb +2709 -0
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/markdown_exec.rb +137 -1458
- data/lib/mdoc.rb +0 -3
- data/lib/menu.src.yml +137 -27
- data/lib/menu.yml +131 -23
- data/lib/method_sorter.rb +19 -17
- data/lib/object_present.rb +1 -1
- data/lib/option_value.rb +4 -2
- data/lib/pty1.rb +16 -16
- data/lib/regexp.rb +4 -5
- data/lib/saved_assets.rb +4 -2
- data/lib/saved_files_matcher.rb +7 -3
- data/lib/shared.rb +0 -5
- data/lib/string_util.rb +22 -0
- data/lib/tap.rb +5 -2
- metadata +10 -3
- data/lib/environment_opt_parse.rb +0 -209
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,249 +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
|
-
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?
|
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)
|
568
183
|
|
569
|
-
|
570
|
-
end
|
184
|
+
# return if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
|
571
185
|
|
572
|
-
#
|
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)
|
186
|
+
# FileUtils.rm_f(temp_blocks_file_path)
|
578
187
|
|
579
|
-
|
580
|
-
|
581
|
-
|
188
|
+
# clear_required_file
|
189
|
+
# rescue StandardError
|
190
|
+
# error_handler('delete_required_temp_file')
|
191
|
+
# end
|
582
192
|
|
583
|
-
|
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
|
193
|
+
public
|
601
194
|
|
602
195
|
## Determines the correct filename to use for searching files
|
603
196
|
#
|
@@ -606,8 +199,7 @@ module MarkdownExec
|
|
606
199
|
if specified_filename&.present?
|
607
200
|
return specified_filename if specified_filename.start_with?('/')
|
608
201
|
|
609
|
-
File.join(specified_folder || default_folder,
|
610
|
-
specified_filename)
|
202
|
+
File.join(specified_folder || default_folder, specified_filename)
|
611
203
|
elsif specified_folder&.present?
|
612
204
|
File.join(specified_folder,
|
613
205
|
filetree ? @options[:md_filename_match] : @options[:md_filename_glob])
|
@@ -616,31 +208,19 @@ module MarkdownExec
|
|
616
208
|
end
|
617
209
|
end
|
618
210
|
|
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
|
211
|
+
private
|
626
212
|
|
627
|
-
def
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
)
|
634
|
-
initialize_and_save_execution_output
|
635
|
-
output_execution_summary
|
636
|
-
output_execution_result
|
637
|
-
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
|
638
219
|
|
639
220
|
# Reports and executes block logic
|
640
221
|
def execute_block_logic(files)
|
641
222
|
@options[:filename] = select_document_if_multiple(files)
|
642
|
-
select_approve_and_execute_block
|
643
|
-
struct: true })
|
223
|
+
@options.select_approve_and_execute_block
|
644
224
|
end
|
645
225
|
|
646
226
|
## Executes the block specified in the options
|
@@ -649,12 +229,10 @@ module MarkdownExec
|
|
649
229
|
finalize_cli_argument_processing(rest)
|
650
230
|
@options[:s_cli_rest] = rest
|
651
231
|
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
|
232
|
+
rescue FileMissingError
|
233
|
+
warn "File missing: #{$!}"
|
234
|
+
rescue StandardError
|
235
|
+
error_handler('execute_block_with_error_handling')
|
658
236
|
end
|
659
237
|
|
660
238
|
# Main method to execute a block based on options and block_name
|
@@ -663,47 +241,40 @@ module MarkdownExec
|
|
663
241
|
update_options(options, over: false)
|
664
242
|
|
665
243
|
simple_commands = {
|
666
|
-
doc_glob: -> { fout options[:md_filename_glob] },
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
struct: true)
|
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 },
|
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 },
|
676
248
|
list_recent_output: lambda {
|
677
|
-
fout_list list_recent_output(
|
249
|
+
@fout.fout_list list_recent_output(
|
678
250
|
@options[:saved_stdout_folder],
|
679
251
|
@options[:saved_stdout_glob], @options[:list_count]
|
680
252
|
)
|
681
253
|
},
|
682
254
|
list_recent_scripts: lambda {
|
683
|
-
fout_list list_recent_scripts(
|
255
|
+
@fout.fout_list list_recent_scripts(
|
684
256
|
options[:saved_script_folder],
|
685
257
|
options[:saved_script_glob], options[:list_count]
|
686
258
|
)
|
687
259
|
},
|
688
|
-
pwd: -> { fout File.expand_path('..', __dir__) },
|
260
|
+
pwd: -> { @fout.fout File.expand_path('..', __dir__) },
|
689
261
|
run_last_script: -> { run_last_script },
|
690
262
|
select_recent_output: -> { select_recent_output },
|
691
263
|
select_recent_script: -> { select_recent_script },
|
692
|
-
tab_completions: -> { fout tab_completions },
|
693
|
-
menu_export: -> { fout menu_export }
|
264
|
+
tab_completions: -> { @fout.fout tab_completions },
|
265
|
+
menu_export: -> { @fout.fout menu_export }
|
694
266
|
}
|
695
267
|
|
696
268
|
return if execute_simple_commands(simple_commands)
|
697
269
|
|
698
|
-
files =
|
270
|
+
files = opts_prepare_file_list(options)
|
699
271
|
execute_block_logic(files)
|
700
272
|
return unless @options[:output_saved_script_filename]
|
701
273
|
|
702
|
-
fout "
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
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')
|
707
278
|
end
|
708
279
|
|
709
280
|
# Executes command based on the provided option keys
|
@@ -736,187 +307,22 @@ module MarkdownExec
|
|
736
307
|
#
|
737
308
|
block_name = rest.shift
|
738
309
|
@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")
|
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')
|
914
318
|
end
|
915
319
|
|
916
320
|
## Sets up the options and returns the parsed arguments
|
917
321
|
#
|
918
322
|
def initialize_and_parse_cli_options
|
919
|
-
@options = base_options
|
323
|
+
# @options = base_options
|
324
|
+
@options = HashDelegator.new(base_options)
|
325
|
+
|
920
326
|
read_configuration_file!(@options,
|
921
327
|
".#{MarkdownExec::APP_NAME.downcase}.yml")
|
922
328
|
|
@@ -929,7 +335,7 @@ module MarkdownExec
|
|
929
335
|
].join("\n")
|
930
336
|
|
931
337
|
menu_iter do |item|
|
932
|
-
|
338
|
+
opts_menu_option_append opts, @options, item
|
933
339
|
end
|
934
340
|
end
|
935
341
|
@option_parser.load
|
@@ -937,55 +343,11 @@ module MarkdownExec
|
|
937
343
|
|
938
344
|
rest = @option_parser.parse!(arguments_for_mde)
|
939
345
|
@options[:s_pass_args] = ARGV[rest.count + 1..]
|
346
|
+
@options.merge(@options.run_state.to_h)
|
940
347
|
|
941
348
|
rest
|
942
349
|
end
|
943
350
|
|
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
351
|
##
|
990
352
|
# Returns a lambda expression based on the given procname.
|
991
353
|
# @param procname [String] The name of the process to generate a lambda for.
|
@@ -1001,7 +363,7 @@ module MarkdownExec
|
|
1001
363
|
->(_) { exit }
|
1002
364
|
when 'help'
|
1003
365
|
lambda { |_|
|
1004
|
-
fout menu_help
|
366
|
+
@fout.fout menu_help
|
1005
367
|
exit
|
1006
368
|
}
|
1007
369
|
when 'path'
|
@@ -1009,7 +371,7 @@ module MarkdownExec
|
|
1009
371
|
when 'show_config'
|
1010
372
|
lambda { |_|
|
1011
373
|
finalize_cli_argument_processing(options)
|
1012
|
-
fout options.sort_by_key.to_yaml
|
374
|
+
@fout.fout options.sort_by_key.to_yaml
|
1013
375
|
}
|
1014
376
|
when 'val_as_bool'
|
1015
377
|
lambda { |value|
|
@@ -1021,7 +383,7 @@ module MarkdownExec
|
|
1021
383
|
->(value) { value.to_s }
|
1022
384
|
when 'version'
|
1023
385
|
lambda { |_|
|
1024
|
-
fout MarkdownExec::VERSION
|
386
|
+
@fout.fout MarkdownExec::VERSION
|
1025
387
|
exit
|
1026
388
|
}
|
1027
389
|
else
|
@@ -1051,16 +413,7 @@ module MarkdownExec
|
|
1051
413
|
end.compact.sort
|
1052
414
|
end
|
1053
415
|
|
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
|
416
|
+
public
|
1064
417
|
|
1065
418
|
## Searches for files based on the specified or default filenames and folders
|
1066
419
|
#
|
@@ -1077,80 +430,7 @@ module MarkdownExec
|
|
1077
430
|
@options[:md_filename_glob]))
|
1078
431
|
end
|
1079
432
|
|
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
|
433
|
+
private
|
1154
434
|
|
1155
435
|
##
|
1156
436
|
# Generates a menu suitable for OptionParser from the menu items defined in YAML format.
|
@@ -1164,37 +444,6 @@ module MarkdownExec
|
|
1164
444
|
end
|
1165
445
|
end
|
1166
446
|
|
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
447
|
def menu_help
|
1199
448
|
@option_parser.help
|
1200
449
|
end
|
@@ -1203,10 +452,26 @@ module MarkdownExec
|
|
1203
452
|
data.map(&block)
|
1204
453
|
end
|
1205
454
|
|
1206
|
-
def
|
1207
|
-
|
1208
|
-
|
1209
|
-
|
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?
|
1210
475
|
|
1211
476
|
opts.on(*[
|
1212
477
|
# - long name
|
@@ -1234,188 +499,9 @@ module MarkdownExec
|
|
1234
499
|
].compact)
|
1235
500
|
end
|
1236
501
|
|
1237
|
-
def menu_with_block_labels(call_options = {})
|
1238
|
-
opts = options.merge(call_options)
|
1239
|
-
menu_from_file(opts).map do |fcb|
|
1240
|
-
BlockLabel.make(
|
1241
|
-
filename: opts[:filename],
|
1242
|
-
headings: fcb.fetch(:headings, []),
|
1243
|
-
menu_blocks_with_docname: opts[:menu_blocks_with_docname],
|
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
|
-
)
|
1329
|
-
)
|
1330
|
-
fcb.to_h
|
1331
|
-
end.compact
|
1332
|
-
end
|
1333
|
-
|
1334
502
|
# Prepares and fetches file listings
|
1335
|
-
def
|
1336
|
-
|
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
|
503
|
+
def opts_prepare_file_list(options)
|
504
|
+
opts_list_files(options)
|
1419
505
|
end
|
1420
506
|
|
1421
507
|
# :reek:UtilityFunction ### temp
|
@@ -1426,53 +512,18 @@ module MarkdownExec
|
|
1426
512
|
.transform_keys(&:to_sym))
|
1427
513
|
end
|
1428
514
|
|
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
|
515
|
+
public
|
1465
516
|
|
1466
517
|
def run
|
1467
518
|
clear_required_file
|
1468
519
|
execute_block_with_error_handling(initialize_and_parse_cli_options)
|
1469
|
-
delete_required_temp_file
|
1470
|
-
rescue StandardError
|
1471
|
-
|
1472
|
-
binding.pry if $tap_enable
|
1473
|
-
raise ArgumentError, error
|
520
|
+
@options.delete_required_temp_file
|
521
|
+
rescue StandardError
|
522
|
+
error_handler('run')
|
1474
523
|
end
|
1475
524
|
|
525
|
+
private
|
526
|
+
|
1476
527
|
def run_last_script
|
1477
528
|
filename = SavedFilesMatcher.most_recent(@options[:saved_script_folder],
|
1478
529
|
@options[:saved_script_glob])
|
@@ -1480,89 +531,20 @@ module MarkdownExec
|
|
1480
531
|
|
1481
532
|
saved_name_split filename
|
1482
533
|
@options[:save_executed_script] = false
|
1483
|
-
select_approve_and_execute_block
|
1484
|
-
end
|
1485
|
-
|
1486
|
-
def safeval(str)
|
1487
|
-
eval(str)
|
534
|
+
@options.select_approve_and_execute_block
|
1488
535
|
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]}"
|
536
|
+
error_handler('run_last_script')
|
1498
537
|
end
|
1499
538
|
|
1500
539
|
def saved_name_split(name)
|
1501
540
|
# rubocop:disable Layout/LineLength
|
1502
|
-
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)
|
1503
542
|
# rubocop:enable Layout/LineLength
|
1504
543
|
return unless mf
|
1505
544
|
|
1506
545
|
@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
|
546
|
+
@options[:filename] = mf[:file].gsub(@options[:saved_filename_pattern],
|
547
|
+
@options[:saved_filename_replacement])
|
1566
548
|
end
|
1567
549
|
|
1568
550
|
def select_document_if_multiple(files = list_markdown_files_in_path)
|
@@ -1571,44 +553,24 @@ module MarkdownExec
|
|
1571
553
|
return unless count >= 2
|
1572
554
|
|
1573
555
|
opts = options.dup
|
1574
|
-
select_option_or_exit
|
1575
|
-
|
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]))
|
1576
559
|
end
|
1577
560
|
|
1578
561
|
# 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,
|
562
|
+
def select_option_or_exit(prompt_text, strings, opts = {})
|
563
|
+
result = @options.select_option_with_metadata(prompt_text, strings,
|
564
|
+
opts)
|
1581
565
|
return unless result.fetch(:option, nil)
|
1582
566
|
|
1583
567
|
result[:selected]
|
1584
568
|
end
|
1585
569
|
|
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
570
|
def select_recent_output
|
1610
571
|
filename = select_option_or_exit(
|
1611
|
-
@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),
|
1612
574
|
list_recent_output(
|
1613
575
|
@options[:saved_stdout_folder],
|
1614
576
|
@options[:saved_stdout_glob],
|
@@ -1623,7 +585,8 @@ module MarkdownExec
|
|
1623
585
|
|
1624
586
|
def select_recent_script
|
1625
587
|
filename = select_option_or_exit(
|
1626
|
-
@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),
|
1627
590
|
list_recent_scripts(
|
1628
591
|
@options[:saved_script_folder],
|
1629
592
|
@options[:saved_script_glob],
|
@@ -1635,38 +598,10 @@ module MarkdownExec
|
|
1635
598
|
|
1636
599
|
saved_name_split(filename)
|
1637
600
|
|
1638
|
-
select_approve_and_execute_block({
|
1639
|
-
save_executed_script: false,
|
1640
|
-
struct: true })
|
601
|
+
@options.select_approve_and_execute_block ### ({ save_executed_script: false })
|
1641
602
|
end
|
1642
603
|
|
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
|
604
|
+
public
|
1670
605
|
|
1671
606
|
def tab_completions(data = menu_for_optparse)
|
1672
607
|
data.map do |item|
|
@@ -1674,89 +609,6 @@ module MarkdownExec
|
|
1674
609
|
end.compact
|
1675
610
|
end
|
1676
611
|
|
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
612
|
# :reek:BooleanParameter
|
1761
613
|
# :reek:ControlParameter
|
1762
614
|
def update_options(opts = {}, over: true)
|
@@ -1767,125 +619,6 @@ module MarkdownExec
|
|
1767
619
|
end
|
1768
620
|
@options
|
1769
621
|
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
622
|
end # class MarkParse
|
1890
623
|
end # module MarkdownExec
|
1891
624
|
|
@@ -1896,60 +629,6 @@ if $PROGRAM_NAME == __FILE__
|
|
1896
629
|
require 'minitest/autorun'
|
1897
630
|
|
1898
631
|
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
632
|
def test_select_block
|
1954
633
|
blocks = [block1, block2]
|
1955
634
|
menu = [m1, m2]
|