markdown_exec 1.3.9 → 1.4
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/.pryrc +11 -0
- data/.rubocop.yml +15 -1
- data/Gemfile.lock +1 -1
- data/bin/bmde +11 -0
- data/bin/tab_completion.sh +2 -2
- data/examples/linked1.md +13 -13
- data/examples/linked2.md +15 -14
- data/examples/linked3.md +12 -0
- data/lib/block_types.rb +2 -0
- data/lib/colorize.rb +52 -55
- data/lib/filter.rb +3 -3
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/markdown_exec.rb +873 -614
- data/lib/mdoc.rb +53 -51
- data/lib/menu.src.yml +19 -1
- data/lib/menu.yml +20 -2
- data/lib/method_sorter.rb +76 -0
- data/lib/sort_yaml_gpt4.rb +32 -0
- metadata +7 -2
data/lib/markdown_exec.rb
CHANGED
@@ -19,13 +19,13 @@ require_relative 'colorize'
|
|
19
19
|
require_relative 'env'
|
20
20
|
require_relative 'fcb'
|
21
21
|
require_relative 'filter'
|
22
|
+
require_relative 'markdown_exec/version'
|
22
23
|
require_relative 'mdoc'
|
23
24
|
require_relative 'option_value'
|
24
25
|
require_relative 'saved_assets'
|
25
26
|
require_relative 'saved_files_matcher'
|
26
27
|
require_relative 'shared'
|
27
28
|
require_relative 'tap'
|
28
|
-
require_relative 'markdown_exec/version'
|
29
29
|
|
30
30
|
include CLI
|
31
31
|
include Tap
|
@@ -35,14 +35,11 @@ tap_config envvar: MarkdownExec::TAP_DEBUG
|
|
35
35
|
$stderr.sync = true
|
36
36
|
$stdout.sync = true
|
37
37
|
|
38
|
-
|
38
|
+
MDE_HISTORY_ENV_NAME = 'MDE_MENU_HISTORY'
|
39
39
|
|
40
40
|
# macros
|
41
41
|
#
|
42
|
-
BACK_OPTION = '* Back'
|
43
|
-
EXIT_OPTION = '* Exit'
|
44
42
|
LOAD_FILE = true
|
45
|
-
VN = 'MDE_MENU_HISTORY'
|
46
43
|
|
47
44
|
# custom error: file specified is missing
|
48
45
|
#
|
@@ -129,10 +126,45 @@ end
|
|
129
126
|
# convert regex match groups to a hash with symbol keys
|
130
127
|
#
|
131
128
|
# :reek:UtilityFunction
|
132
|
-
def
|
129
|
+
def extract_named_captures_from_option(str, option)
|
133
130
|
str.match(Regexp.new(option))&.named_captures&.sym_keys
|
134
131
|
end
|
135
132
|
|
133
|
+
module ArrayUtil
|
134
|
+
def self.partition_by_predicate(arr)
|
135
|
+
true_list = []
|
136
|
+
false_list = []
|
137
|
+
|
138
|
+
arr.each do |element|
|
139
|
+
if yield(element)
|
140
|
+
true_list << element
|
141
|
+
else
|
142
|
+
false_list << element
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
[true_list, false_list]
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
module StringUtil
|
151
|
+
# Splits the given string on the first occurrence of the specified character.
|
152
|
+
# Returns an array containing the portion of the string before the character and the rest of the string.
|
153
|
+
#
|
154
|
+
# @param input_str [String] The string to be split.
|
155
|
+
# @param split_char [String] The character on which to split the string.
|
156
|
+
# @return [Array<String>] An array containing two elements: the part of the string before split_char, and the rest of the string.
|
157
|
+
def self.partition_at_first(input_str, split_char)
|
158
|
+
split_index = input_str.index(split_char)
|
159
|
+
|
160
|
+
if split_index.nil?
|
161
|
+
[input_str, '']
|
162
|
+
else
|
163
|
+
[input_str[0...split_index], input_str[(split_index + 1)..-1]]
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
136
168
|
# execute markdown documents
|
137
169
|
#
|
138
170
|
module MarkdownExec
|
@@ -141,10 +173,10 @@ module MarkdownExec
|
|
141
173
|
FNR12 = ',~'
|
142
174
|
|
143
175
|
SHELL_COLOR_OPTIONS = {
|
144
|
-
|
176
|
+
BLOCK_TYPE_BASH => :menu_bash_color,
|
145
177
|
BLOCK_TYPE_LINK => :menu_link_color,
|
146
|
-
|
147
|
-
|
178
|
+
BLOCK_TYPE_OPTS => :menu_opts_color,
|
179
|
+
BLOCK_TYPE_VARS => :menu_vars_color
|
148
180
|
}.freeze
|
149
181
|
|
150
182
|
##
|
@@ -158,12 +190,11 @@ module MarkdownExec
|
|
158
190
|
class MarkParse
|
159
191
|
attr_reader :options
|
160
192
|
|
193
|
+
include ArrayUtil
|
194
|
+
include StringUtil
|
161
195
|
include FOUT
|
162
196
|
|
163
197
|
def initialize(options = {})
|
164
|
-
@options = options
|
165
|
-
# hide disabled symbol
|
166
|
-
@prompt = TTY::Prompt.new(interrupt: :exit, symbols: { cross: ' ' })
|
167
198
|
@execute_aborted_at = nil
|
168
199
|
@execute_completed_at = nil
|
169
200
|
@execute_error = nil
|
@@ -173,6 +204,74 @@ module MarkdownExec
|
|
173
204
|
@execute_script_filespec = nil
|
174
205
|
@execute_started_at = nil
|
175
206
|
@option_parser = nil
|
207
|
+
@options = options
|
208
|
+
@prompt = tty_prompt_without_disabled_symbol
|
209
|
+
end
|
210
|
+
|
211
|
+
##
|
212
|
+
# Appends a summary of a block (FCB) to the blocks array.
|
213
|
+
#
|
214
|
+
def append_block_summary(blocks, fcb, opts)
|
215
|
+
## enhance fcb with block summary
|
216
|
+
#
|
217
|
+
blocks.push get_block_summary(opts, fcb)
|
218
|
+
end
|
219
|
+
|
220
|
+
##
|
221
|
+
# Appends a final divider to the blocks array if it is specified in options.
|
222
|
+
#
|
223
|
+
def append_final_divider(blocks, opts)
|
224
|
+
return unless opts[:menu_divider_format].present? && opts[:menu_final_divider].present?
|
225
|
+
|
226
|
+
blocks.push FCB.new(
|
227
|
+
{ chrome: true,
|
228
|
+
disabled: '',
|
229
|
+
dname: format(opts[:menu_divider_format],
|
230
|
+
opts[:menu_final_divider])
|
231
|
+
.send(opts[:menu_divider_color].to_sym),
|
232
|
+
oname: opts[:menu_final_divider] }
|
233
|
+
)
|
234
|
+
end
|
235
|
+
|
236
|
+
##
|
237
|
+
# Appends an initial divider to the blocks array if it is specified in options.
|
238
|
+
#
|
239
|
+
def append_initial_divider(blocks, opts)
|
240
|
+
return unless opts[:menu_initial_divider].present?
|
241
|
+
|
242
|
+
blocks.push FCB.new({
|
243
|
+
# name: '',
|
244
|
+
chrome: true,
|
245
|
+
dname: format(
|
246
|
+
opts[:menu_divider_format],
|
247
|
+
opts[:menu_initial_divider]
|
248
|
+
).send(opts[:menu_divider_color].to_sym),
|
249
|
+
oname: opts[:menu_initial_divider],
|
250
|
+
disabled: '' # __LINE__.to_s
|
251
|
+
})
|
252
|
+
end
|
253
|
+
|
254
|
+
# Execute a code block after approval and provide user interaction options.
|
255
|
+
#
|
256
|
+
# This method displays required code blocks, asks for user approval, and
|
257
|
+
# executes the code block if approved. It also allows users to copy the
|
258
|
+
# code to the clipboard or save it to a file.
|
259
|
+
#
|
260
|
+
# @param opts [Hash] Options hash containing configuration settings.
|
261
|
+
# @param mdoc [YourMDocClass] An instance of the MDoc class.
|
262
|
+
#
|
263
|
+
def approve_and_execute_block(opts, mdoc)
|
264
|
+
selected = mdoc.get_block_by_name(opts[:block_name])
|
265
|
+
|
266
|
+
if selected.fetch(:shell, '') == BLOCK_TYPE_LINK
|
267
|
+
handle_shell_link(opts, selected.fetch(:body, ''), mdoc)
|
268
|
+
elsif opts.fetch(:back, false)
|
269
|
+
handle_back_link(opts)
|
270
|
+
elsif selected[:shell] == BLOCK_TYPE_OPTS
|
271
|
+
handle_shell_opts(opts, selected)
|
272
|
+
else
|
273
|
+
handle_remainder_blocks(mdoc, opts, selected)
|
274
|
+
end
|
176
275
|
end
|
177
276
|
|
178
277
|
# return arguments before `--`
|
@@ -206,6 +305,14 @@ module MarkdownExec
|
|
206
305
|
end.compact.to_h
|
207
306
|
end
|
208
307
|
|
308
|
+
def blocks_per_opts(blocks, opts)
|
309
|
+
return blocks if opts[:struct]
|
310
|
+
|
311
|
+
blocks.map do |block|
|
312
|
+
block.fetch(:text, nil) || block.oname
|
313
|
+
end.compact.reject(&:empty?)
|
314
|
+
end
|
315
|
+
|
209
316
|
def calculated_options
|
210
317
|
{
|
211
318
|
bash: true, # bash block parsing in get_block_summary()
|
@@ -214,100 +321,22 @@ module MarkdownExec
|
|
214
321
|
}
|
215
322
|
end
|
216
323
|
|
217
|
-
#
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
#
|
223
|
-
# @param opts [Hash] Options hash containing configuration settings.
|
224
|
-
# @param mdoc [YourMDocClass] An instance of the MDoc class.
|
225
|
-
# @return [String] The name of the executed code block.
|
226
|
-
#
|
227
|
-
def approve_and_execute_block(opts, mdoc)
|
228
|
-
selected = mdoc.get_block_by_name(opts[:block_name])
|
229
|
-
if selected[:shell] == BLOCK_TYPE_LINK
|
230
|
-
handle_link_shell(opts, selected)
|
231
|
-
elsif selected[:shell] == 'opts'
|
232
|
-
handle_opts_shell(opts, selected)
|
233
|
-
else
|
234
|
-
required_lines = collect_required_code_blocks(opts, mdoc, selected)
|
235
|
-
# Display required code blocks if requested or required approval.
|
236
|
-
if opts[:output_script] || opts[:user_must_approve]
|
237
|
-
display_required_code(opts, required_lines)
|
238
|
-
end
|
239
|
-
|
240
|
-
allow = true
|
241
|
-
allow = user_approval(opts, required_lines) if opts[:user_must_approve]
|
242
|
-
opts[:ir_approve] = allow
|
243
|
-
mdoc.get_block_by_name(opts[:block_name])
|
244
|
-
execute_approved_block(opts, required_lines) if opts[:ir_approve]
|
245
|
-
|
246
|
-
[!LOAD_FILE, '']
|
247
|
-
end
|
248
|
-
end
|
249
|
-
|
250
|
-
def handle_link_shell(opts, selected)
|
251
|
-
data = YAML.load(selected[:body].join("\n"))
|
252
|
-
|
253
|
-
# add to front of history
|
254
|
-
#
|
255
|
-
ENV[VN] = opts[:filename] + opts[:history_document_separator] + ENV.fetch(VN, '')
|
256
|
-
|
257
|
-
opts[:filename] = data.fetch('file', nil)
|
258
|
-
return !LOAD_FILE unless opts[:filename]
|
259
|
-
|
260
|
-
data.fetch('vars', []).each do |var|
|
261
|
-
ENV[var[0]] = var[1].to_s
|
262
|
-
end
|
263
|
-
|
264
|
-
[LOAD_FILE, data.fetch('block', '')]
|
265
|
-
end
|
266
|
-
|
267
|
-
def handle_opts_shell(opts, selected)
|
268
|
-
data = YAML.load(selected[:body].join("\n"))
|
269
|
-
data.each_key do |key|
|
270
|
-
opts[key.to_sym] = value = data[key].to_s
|
271
|
-
next unless opts[:menu_opts_set_format].present?
|
272
|
-
|
273
|
-
print format(
|
274
|
-
opts[:menu_opts_set_format],
|
275
|
-
{ key: key,
|
276
|
-
value: value }
|
277
|
-
).send(opts[:menu_opts_set_color].to_sym)
|
278
|
-
end
|
279
|
-
[!LOAD_FILE, '']
|
280
|
-
end
|
281
|
-
|
282
|
-
def user_approval(opts, required_lines)
|
283
|
-
# Present a selection menu for user approval.
|
284
|
-
sel = @prompt.select(opts[:prompt_approve_block], filter: true) do |menu|
|
285
|
-
menu.default 1
|
286
|
-
menu.choice opts[:prompt_yes], 1
|
287
|
-
menu.choice opts[:prompt_no], 2
|
288
|
-
menu.choice opts[:prompt_script_to_clipboard], 3
|
289
|
-
menu.choice opts[:prompt_save_script], 4
|
324
|
+
# Check whether the document exists and is readable
|
325
|
+
def check_file_existence(filename)
|
326
|
+
unless filename&.present?
|
327
|
+
fout 'No blocks found.'
|
328
|
+
return false
|
290
329
|
end
|
291
330
|
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
save_to_file(opts, required_lines)
|
331
|
+
unless File.exist? filename
|
332
|
+
fout 'Document is missing.'
|
333
|
+
return false
|
296
334
|
end
|
297
|
-
|
298
|
-
sel == 1
|
335
|
+
true
|
299
336
|
end
|
300
337
|
|
301
|
-
def
|
302
|
-
|
303
|
-
command_execute(
|
304
|
-
opts,
|
305
|
-
required_lines.flatten.join("\n"),
|
306
|
-
args: opts.fetch(:pass_args, [])
|
307
|
-
)
|
308
|
-
save_execution_output
|
309
|
-
output_execution_summary
|
310
|
-
output_execution_result
|
338
|
+
def clear_required_file
|
339
|
+
ENV['MDE_LINK_REQUIRED_FILE'] = ''
|
311
340
|
end
|
312
341
|
|
313
342
|
# Collect required code blocks based on the provided options.
|
@@ -315,11 +344,7 @@ module MarkdownExec
|
|
315
344
|
# @param opts [Hash] Options hash containing configuration settings.
|
316
345
|
# @param mdoc [YourMDocClass] An instance of the MDoc class.
|
317
346
|
# @return [Array<String>] Required code blocks as an array of lines.
|
318
|
-
def
|
319
|
-
required = mdoc.collect_recursively_required_code(opts[:block_name])
|
320
|
-
required_lines = required[:code]
|
321
|
-
required[:blocks]
|
322
|
-
|
347
|
+
def collect_required_code_lines(mdoc, selected, opts: {})
|
323
348
|
# Apply hash in opts block to environment variables
|
324
349
|
if selected[:shell] == BLOCK_TYPE_VARS
|
325
350
|
data = YAML.load(selected[:body].join("\n"))
|
@@ -335,33 +360,20 @@ module MarkdownExec
|
|
335
360
|
end
|
336
361
|
end
|
337
362
|
|
338
|
-
|
363
|
+
required = mdoc.collect_recursively_required_code(opts[:block_name], opts: opts)
|
364
|
+
read_required_blocks_from_temp_file + required[:code]
|
339
365
|
end
|
340
366
|
|
341
367
|
def cfile
|
342
|
-
@cfile ||= CachedNestedFileReader.new(
|
368
|
+
@cfile ||= CachedNestedFileReader.new(
|
369
|
+
import_pattern: @options.fetch(:import_pattern)
|
370
|
+
)
|
343
371
|
end
|
344
372
|
|
345
373
|
EF_STDOUT = :stdout
|
346
374
|
EF_STDERR = :stderr
|
347
375
|
EF_STDIN = :stdin
|
348
376
|
|
349
|
-
# Handles reading and processing lines from a given IO stream
|
350
|
-
#
|
351
|
-
# @param stream [IO] The IO stream to read from (e.g., stdout, stderr, stdin).
|
352
|
-
# @param file_type [Symbol] The type of file to which the stream corresponds.
|
353
|
-
def handle_stream(opts, stream, file_type, swap: false)
|
354
|
-
Thread.new do
|
355
|
-
until (line = stream.gets).nil?
|
356
|
-
@execute_files[file_type] = @execute_files[file_type] + [line.strip]
|
357
|
-
print line if opts[:output_stdout]
|
358
|
-
yield line if block_given?
|
359
|
-
end
|
360
|
-
rescue IOError
|
361
|
-
#d 'stdout IOError, thread killed, do nothing'
|
362
|
-
end
|
363
|
-
end
|
364
|
-
|
365
377
|
# Existing command_execute method
|
366
378
|
def command_execute(opts, command, args: [])
|
367
379
|
@execute_files = Hash.new([])
|
@@ -404,6 +416,14 @@ module MarkdownExec
|
|
404
416
|
fout "Error ENOENT: #{err.inspect}"
|
405
417
|
end
|
406
418
|
|
419
|
+
def copy_to_clipboard(required_lines)
|
420
|
+
text = required_lines.flatten.join($INPUT_RECORD_SEPARATOR)
|
421
|
+
Clipboard.copy(text)
|
422
|
+
fout "Clipboard updated: #{required_lines.count} blocks," \
|
423
|
+
" #{required_lines.flatten.count} lines," \
|
424
|
+
" #{text.length} characters"
|
425
|
+
end
|
426
|
+
|
407
427
|
def count_blocks_in_filename
|
408
428
|
fenced_start_and_end_regex = Regexp.new @options[:fenced_start_and_end_regex]
|
409
429
|
cnt = 0
|
@@ -413,6 +433,45 @@ module MarkdownExec
|
|
413
433
|
cnt / 2
|
414
434
|
end
|
415
435
|
|
436
|
+
def create_and_write_file_with_permissions(file_path, content, chmod_value)
|
437
|
+
dirname = File.dirname(file_path)
|
438
|
+
FileUtils.mkdir_p dirname
|
439
|
+
File.write(file_path, content)
|
440
|
+
return if chmod_value.zero?
|
441
|
+
|
442
|
+
File.chmod chmod_value, file_path
|
443
|
+
end
|
444
|
+
|
445
|
+
# Deletes a required temporary file specified by an environment variable.
|
446
|
+
# The function checks if the file exists before attempting to delete it.
|
447
|
+
# Clears the environment variable after deletion.
|
448
|
+
#
|
449
|
+
def delete_required_temp_file
|
450
|
+
temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
|
451
|
+
|
452
|
+
return if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
|
453
|
+
|
454
|
+
FileUtils.rm_f(temp_blocks_file_path)
|
455
|
+
|
456
|
+
clear_required_file
|
457
|
+
end
|
458
|
+
|
459
|
+
## Determines the correct filename to use for searching files
|
460
|
+
#
|
461
|
+
def determine_filename(specified_filename: nil, specified_folder: nil, default_filename: nil,
|
462
|
+
default_folder: nil, filetree: nil)
|
463
|
+
if specified_filename&.present?
|
464
|
+
return specified_filename if specified_filename.start_with?('/')
|
465
|
+
|
466
|
+
File.join(specified_folder || default_folder, specified_filename)
|
467
|
+
elsif specified_folder&.present?
|
468
|
+
File.join(specified_folder,
|
469
|
+
filetree ? @options[:md_filename_match] : @options[:md_filename_glob])
|
470
|
+
else
|
471
|
+
File.join(default_folder, default_filename)
|
472
|
+
end
|
473
|
+
end
|
474
|
+
|
416
475
|
# :reek:DuplicateMethodCall
|
417
476
|
def display_required_code(opts, required_lines)
|
418
477
|
frame = opts[:output_divider].send(opts[:output_divider_color].to_sym)
|
@@ -421,14 +480,40 @@ module MarkdownExec
|
|
421
480
|
fout frame
|
422
481
|
end
|
423
482
|
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
483
|
+
def execute_approved_block(opts, required_lines)
|
484
|
+
write_command_file(opts, required_lines)
|
485
|
+
command_execute(
|
486
|
+
opts,
|
487
|
+
required_lines.flatten.join("\n"),
|
488
|
+
args: opts.fetch(:pass_args, [])
|
489
|
+
)
|
490
|
+
initialize_and_save_execution_output
|
491
|
+
output_execution_summary
|
492
|
+
output_execution_result
|
493
|
+
end
|
428
494
|
|
429
|
-
|
430
|
-
|
431
|
-
|
495
|
+
# Reports and executes block logic
|
496
|
+
def execute_block_logic(files)
|
497
|
+
@options[:filename] = select_document_if_multiple(files)
|
498
|
+
select_approve_and_execute_block({
|
499
|
+
bash: true,
|
500
|
+
struct: true
|
501
|
+
})
|
502
|
+
end
|
503
|
+
|
504
|
+
## Executes the block specified in the options
|
505
|
+
#
|
506
|
+
def execute_block_with_error_handling(rest)
|
507
|
+
finalize_cli_argument_processing(rest)
|
508
|
+
execute_code_block_based_on_options(@options, @options[:block_name])
|
509
|
+
rescue FileMissingError => err
|
510
|
+
puts "File missing: #{err}"
|
511
|
+
end
|
512
|
+
|
513
|
+
# Main method to execute a block based on options and block_name
|
514
|
+
def execute_code_block_based_on_options(options, _block_name = '')
|
515
|
+
options = calculated_options.merge(options)
|
516
|
+
update_options(options, over: false)
|
432
517
|
|
433
518
|
simple_commands = {
|
434
519
|
doc_glob: -> { fout options[:md_filename_glob] },
|
@@ -459,27 +544,59 @@ module MarkdownExec
|
|
459
544
|
tab_completions: -> { fout tab_completions },
|
460
545
|
menu_export: -> { fout menu_export }
|
461
546
|
}
|
547
|
+
|
548
|
+
return if execute_simple_commands(simple_commands)
|
549
|
+
|
550
|
+
files = prepare_file_list(options)
|
551
|
+
execute_block_logic(files)
|
552
|
+
return unless @options[:output_saved_script_filename]
|
553
|
+
|
554
|
+
fout "saved_filespec: #{@execute_script_filespec}"
|
555
|
+
rescue StandardError => err
|
556
|
+
warn(error = "ERROR ** MarkParse.execute_code_block_based_on_options(); #{err.inspect}")
|
557
|
+
binding.pry if $tap_enable
|
558
|
+
raise ArgumentError, error
|
559
|
+
end
|
560
|
+
|
561
|
+
# Executes command based on the provided option keys
|
562
|
+
def execute_simple_commands(simple_commands)
|
462
563
|
simple_commands.each_key do |key|
|
463
564
|
if @options[key]
|
464
565
|
simple_commands[key].call
|
465
|
-
return
|
566
|
+
return true
|
466
567
|
end
|
467
568
|
end
|
569
|
+
false
|
570
|
+
end
|
571
|
+
|
572
|
+
##
|
573
|
+
# Determines the types of blocks to select based on the filter.
|
574
|
+
#
|
575
|
+
def filter_block_types
|
576
|
+
## return type of blocks to select
|
577
|
+
#
|
578
|
+
%i[blocks line]
|
579
|
+
end
|
468
580
|
|
469
|
-
|
581
|
+
## post-parse options configuration
|
582
|
+
#
|
583
|
+
def finalize_cli_argument_processing(rest)
|
584
|
+
## position 0: file or folder (optional)
|
470
585
|
#
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
586
|
+
if (pos = rest.shift)&.present?
|
587
|
+
if Dir.exist?(pos)
|
588
|
+
@options[:path] = pos
|
589
|
+
elsif File.exist?(pos)
|
590
|
+
@options[:filename] = pos
|
591
|
+
else
|
592
|
+
raise FileMissingError, pos, caller
|
593
|
+
end
|
594
|
+
end
|
477
595
|
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
raise ArgumentError, error
|
596
|
+
## position 1: block name (optional)
|
597
|
+
#
|
598
|
+
block_name = rest.shift
|
599
|
+
@options[:block_name] = block_name if block_name.present?
|
483
600
|
end
|
484
601
|
|
485
602
|
## summarize blocks
|
@@ -495,9 +612,9 @@ module MarkdownExec
|
|
495
612
|
else
|
496
613
|
fcb.title
|
497
614
|
end
|
498
|
-
bm =
|
499
|
-
fcb.stdin =
|
500
|
-
fcb.stdout =
|
615
|
+
bm = extract_named_captures_from_option(titlexcall, opts[:block_name_match])
|
616
|
+
fcb.stdin = extract_named_captures_from_option(titlexcall, opts[:block_stdin_scan])
|
617
|
+
fcb.stdout = extract_named_captures_from_option(titlexcall, opts[:block_stdout_scan])
|
501
618
|
|
502
619
|
shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
|
503
620
|
fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall
|
@@ -509,218 +626,268 @@ module MarkdownExec
|
|
509
626
|
fcb
|
510
627
|
end
|
511
628
|
|
512
|
-
|
513
|
-
#
|
514
|
-
#
|
515
|
-
|
629
|
+
##
|
630
|
+
# Handles errors that occur during the block listing process.
|
631
|
+
#
|
632
|
+
def handle_error(err)
|
633
|
+
warn(error = "ERROR ** MarkParse.list_blocks_in_file(); #{err.inspect}")
|
634
|
+
warn(caller[0..4])
|
635
|
+
raise StandardError, error
|
636
|
+
end
|
516
637
|
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
638
|
+
# Handles the link-back operation.
|
639
|
+
#
|
640
|
+
# @param opts [Hash] Configuration options hash.
|
641
|
+
# @return [Array<Symbol, String>] A tuple containing a LOAD_FILE flag and an empty string.
|
642
|
+
def handle_back_link(opts)
|
643
|
+
history_state_pop(opts)
|
644
|
+
[LOAD_FILE, '']
|
645
|
+
end
|
522
646
|
|
523
|
-
|
524
|
-
|
525
|
-
|
647
|
+
# Handles the execution and display of remainder blocks from a selected menu item.
|
648
|
+
#
|
649
|
+
# @param mdoc [Object] Document object containing code blocks.
|
650
|
+
# @param opts [Hash] Configuration options hash.
|
651
|
+
# @param selected [Hash] Selected item from the menu.
|
652
|
+
# @return [Array<Symbol, String>] A tuple containing a LOAD_FILE flag and an empty string.
|
653
|
+
# @note The function can prompt the user for approval before executing code if opts[:user_must_approve] is true.
|
654
|
+
def handle_remainder_blocks(mdoc, opts, selected)
|
655
|
+
required_lines = collect_required_code_lines(mdoc, selected, opts: opts)
|
656
|
+
if opts[:output_script] || opts[:user_must_approve]
|
657
|
+
display_required_code(opts, required_lines)
|
526
658
|
end
|
659
|
+
allow = opts[:user_must_approve] ? prompt_for_user_approval(opts, required_lines) : true
|
660
|
+
opts[:ir_approve] = allow
|
661
|
+
execute_approved_block(opts, required_lines) if opts[:ir_approve]
|
527
662
|
|
528
|
-
|
529
|
-
|
530
|
-
fcb = FCB.new
|
531
|
-
in_fenced_block = false
|
532
|
-
headings = []
|
663
|
+
[!LOAD_FILE, '']
|
664
|
+
end
|
533
665
|
|
534
|
-
|
535
|
-
|
536
|
-
|
666
|
+
# Handles the link-shell operation.
|
667
|
+
#
|
668
|
+
# @param opts [Hash] Configuration options hash.
|
669
|
+
# @param body [Array<String>] The body content.
|
670
|
+
# @param mdoc [Object] Document object containing code blocks.
|
671
|
+
# @return [Array<Symbol, String>] A tuple containing a LOAD_FILE flag and a block name.
|
672
|
+
def handle_shell_link(opts, body, mdoc)
|
673
|
+
data = body.present? ? YAML.load(body.join("\n")) : {}
|
674
|
+
data_file = data.fetch('file', nil)
|
675
|
+
return [!LOAD_FILE, ''] unless data_file
|
537
676
|
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
if line.match(fenced_start_and_end_regex)
|
543
|
-
if in_fenced_block
|
544
|
-
process_fenced_block(fcb, opts, selected_messages, &block)
|
545
|
-
in_fenced_block = false
|
546
|
-
else
|
547
|
-
fcb = start_fenced_block(opts, line, headings, fenced_start_extended_regex)
|
548
|
-
in_fenced_block = true
|
549
|
-
end
|
550
|
-
elsif in_fenced_block && fcb.body
|
551
|
-
dp 'append line to fcb body'
|
552
|
-
fcb.body += [line.chomp]
|
553
|
-
else
|
554
|
-
process_line(line, opts, selected_messages, &block)
|
555
|
-
end
|
677
|
+
history_state_push(mdoc, data_file, opts)
|
678
|
+
|
679
|
+
data.fetch('vars', []).each do |var|
|
680
|
+
ENV[var[0]] = var[1].to_s
|
556
681
|
end
|
682
|
+
|
683
|
+
[LOAD_FILE, data.fetch('block', '')]
|
557
684
|
end
|
558
685
|
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
686
|
+
# Handles options for the shell.
|
687
|
+
#
|
688
|
+
# @param opts [Hash] Configuration options hash.
|
689
|
+
# @param selected [Hash] Selected item from the menu.
|
690
|
+
# @return [Array<Symbol, String>] A tuple containing a NOT_LOAD_FILE flag and an empty string.
|
691
|
+
def handle_shell_opts(opts, selected)
|
692
|
+
data = YAML.load(selected[:body].join("\n"))
|
693
|
+
data.each_key do |key|
|
694
|
+
opts[key.to_sym] = value = data[key].to_s
|
695
|
+
next unless opts[:menu_opts_set_format].present?
|
569
696
|
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
697
|
+
print format(
|
698
|
+
opts[:menu_opts_set_format],
|
699
|
+
{ key: key,
|
700
|
+
value: value }
|
701
|
+
).send(opts[:menu_opts_set_color].to_sym)
|
574
702
|
end
|
575
|
-
|
576
|
-
fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
|
577
|
-
tn.named_captures.sym_keys
|
578
|
-
end
|
579
|
-
fcb.stdout = if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
|
580
|
-
tn.named_captures.sym_keys
|
581
|
-
end
|
582
|
-
fcb
|
703
|
+
[!LOAD_FILE, '']
|
583
704
|
end
|
584
705
|
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
706
|
+
# Handles reading and processing lines from a given IO stream
|
707
|
+
#
|
708
|
+
# @param stream [IO] The IO stream to read from (e.g., stdout, stderr, stdin).
|
709
|
+
# @param file_type [Symbol] The type of file to which the stream corresponds.
|
710
|
+
def handle_stream(opts, stream, file_type, swap: false)
|
711
|
+
Thread.new do
|
712
|
+
until (line = stream.gets).nil?
|
713
|
+
@execute_files[file_type] = @execute_files[file_type] + [line.strip]
|
714
|
+
print line if opts[:output_stdout]
|
715
|
+
yield line if block_given?
|
716
|
+
end
|
717
|
+
rescue IOError
|
718
|
+
#d 'stdout IOError, thread killed, do nothing'
|
595
719
|
end
|
596
720
|
end
|
597
721
|
|
598
|
-
def
|
599
|
-
|
722
|
+
def history_state_exist?
|
723
|
+
history = ENV.fetch(MDE_HISTORY_ENV_NAME, '')
|
724
|
+
history.present? ? history : nil
|
725
|
+
end
|
600
726
|
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
727
|
+
def history_state_partition(opts)
|
728
|
+
unit, rest = StringUtil.partition_at_first(
|
729
|
+
ENV.fetch(MDE_HISTORY_ENV_NAME, ''),
|
730
|
+
opts[:history_document_separator]
|
731
|
+
)
|
732
|
+
{ unit: unit, rest: rest }.tap_inspect
|
605
733
|
end
|
606
734
|
|
607
|
-
|
608
|
-
|
609
|
-
|
735
|
+
def history_state_pop(opts)
|
736
|
+
state = history_state_partition(opts)
|
737
|
+
opts[:filename] = state[:unit]
|
738
|
+
ENV[MDE_HISTORY_ENV_NAME] = state[:rest]
|
739
|
+
delete_required_temp_file
|
740
|
+
end
|
610
741
|
|
611
|
-
|
742
|
+
def history_state_push(mdoc, data_file, opts)
|
743
|
+
[data_file, opts[:block_name]].tap_inspect 'filename, blockname'
|
744
|
+
new_history = opts[:filename] +
|
745
|
+
opts[:history_document_separator] +
|
746
|
+
ENV.fetch(MDE_HISTORY_ENV_NAME, '')
|
747
|
+
opts[:filename] = data_file
|
748
|
+
write_required_blocks_to_temp_file(mdoc, opts[:block_name], opts)
|
749
|
+
ENV[MDE_HISTORY_ENV_NAME] = new_history
|
612
750
|
end
|
613
751
|
|
614
|
-
|
615
|
-
|
616
|
-
|
752
|
+
## Sets up the options and returns the parsed arguments
|
753
|
+
#
|
754
|
+
def initialize_and_parse_cli_options
|
755
|
+
@options = base_options
|
756
|
+
read_configuration_file!(@options, ".#{MarkdownExec::APP_NAME.downcase}.yml")
|
617
757
|
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
758
|
+
@option_parser = OptionParser.new do |opts|
|
759
|
+
executable_name = File.basename($PROGRAM_NAME)
|
760
|
+
opts.banner = [
|
761
|
+
"#{MarkdownExec::APP_NAME}" \
|
762
|
+
" - #{MarkdownExec::APP_DESC} (#{MarkdownExec::VERSION})",
|
763
|
+
"Usage: #{executable_name} [(path | filename [block_name])] [options]"
|
764
|
+
].join("\n")
|
765
|
+
|
766
|
+
menu_iter do |item|
|
767
|
+
menu_option_append opts, @options, item
|
623
768
|
end
|
624
769
|
end
|
770
|
+
@option_parser.load
|
771
|
+
@option_parser.environment
|
625
772
|
|
626
|
-
|
773
|
+
rest = @option_parser.parse!(arguments_for_mde)
|
774
|
+
@options[:pass_args] = ARGV[rest.count + 1..]
|
775
|
+
|
776
|
+
rest
|
627
777
|
end
|
628
778
|
|
629
|
-
def
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
779
|
+
def initialize_and_save_execution_output
|
780
|
+
return unless @options[:save_execution_output]
|
781
|
+
|
782
|
+
@options[:logged_stdout_filename] =
|
783
|
+
SavedAsset.stdout_name(blockname: @options[:block_name],
|
784
|
+
filename: File.basename(@options[:filename], '.*'),
|
785
|
+
prefix: @options[:logged_stdout_filename_prefix],
|
786
|
+
time: Time.now.utc)
|
787
|
+
|
788
|
+
@logged_stdout_filespec =
|
789
|
+
@options[:logged_stdout_filespec] =
|
790
|
+
File.join @options[:saved_stdout_folder],
|
791
|
+
@options[:logged_stdout_filename]
|
792
|
+
@logged_stdout_filespec = @options[:logged_stdout_filespec]
|
793
|
+
write_execution_output_to_file
|
794
|
+
end
|
795
|
+
|
796
|
+
# Initializes variables for regex and other states
|
797
|
+
def initialize_state(opts)
|
798
|
+
{
|
799
|
+
fenced_start_and_end_regex: Regexp.new(opts[:fenced_start_and_end_regex]),
|
800
|
+
fenced_start_extended_regex: Regexp.new(opts[:fenced_start_extended_regex]),
|
801
|
+
fcb: FCB.new,
|
802
|
+
in_fenced_block: false,
|
803
|
+
headings: []
|
804
|
+
}
|
805
|
+
end
|
806
|
+
|
807
|
+
# Main function to iterate through blocks in file
|
808
|
+
def iter_blocks_in_file(opts = {}, &block)
|
809
|
+
return unless check_file_existence(opts[:filename])
|
810
|
+
|
811
|
+
state = initialize_state(opts)
|
812
|
+
|
813
|
+
# get type of messages to select
|
814
|
+
selected_messages = yield :filter
|
815
|
+
|
816
|
+
cfile.readlines(opts[:filename]).each do |line|
|
817
|
+
next unless line
|
818
|
+
|
819
|
+
update_line_and_block_state(line, state, opts, selected_messages, &block)
|
638
820
|
end
|
639
821
|
end
|
640
822
|
|
641
|
-
|
642
|
-
#
|
823
|
+
##
|
824
|
+
# Returns a list of blocks in a given file, including dividers, tasks, and other types of blocks.
|
825
|
+
# The list can be customized via call_options and options_block.
|
826
|
+
#
|
827
|
+
# @param call_options [Hash] Options passed as an argument.
|
828
|
+
# @param options_block [Proc] Block for dynamic option manipulation.
|
829
|
+
# @return [Array<FCB>] An array of FCB objects representing the blocks.
|
643
830
|
#
|
644
831
|
def list_blocks_in_file(call_options = {}, &options_block)
|
645
832
|
opts = optsmerge(call_options, options_block)
|
646
833
|
use_chrome = !opts[:no_chrome]
|
647
834
|
|
648
835
|
blocks = []
|
649
|
-
|
650
|
-
blocks.push FCB.new({
|
651
|
-
# name: '',
|
652
|
-
chrome: true,
|
653
|
-
dname: format(
|
654
|
-
opts[:menu_divider_format],
|
655
|
-
opts[:menu_initial_divider]
|
656
|
-
).send(opts[:menu_divider_color].to_sym),
|
657
|
-
oname: opts[:menu_initial_divider],
|
658
|
-
disabled: '' # __LINE__.to_s
|
659
|
-
})
|
660
|
-
end
|
836
|
+
append_initial_divider(blocks, opts) if use_chrome
|
661
837
|
|
662
838
|
iter_blocks_in_file(opts) do |btype, fcb|
|
663
839
|
case btype
|
664
840
|
when :filter
|
665
|
-
|
666
|
-
#
|
667
|
-
%i[blocks line]
|
668
|
-
|
841
|
+
filter_block_types
|
669
842
|
when :line
|
670
|
-
|
671
|
-
#
|
672
|
-
if opts[:menu_divider_match].present? &&
|
673
|
-
(mbody = fcb.body[0].match opts[:menu_divider_match])
|
674
|
-
if use_chrome
|
675
|
-
blocks.push FCB.new(
|
676
|
-
{ chrome: true,
|
677
|
-
disabled: '',
|
678
|
-
dname: format(opts[:menu_divider_format],
|
679
|
-
mbody[:name]).send(opts[:menu_divider_color].to_sym),
|
680
|
-
oname: mbody[:name] }
|
681
|
-
)
|
682
|
-
end
|
683
|
-
elsif opts[:menu_task_match].present? &&
|
684
|
-
(fcb.body[0].match opts[:menu_task_match])
|
685
|
-
if use_chrome
|
686
|
-
blocks.push FCB.new(
|
687
|
-
{ chrome: true,
|
688
|
-
disabled: '',
|
689
|
-
dname: format(
|
690
|
-
opts[:menu_task_format],
|
691
|
-
$~.named_captures.transform_keys(&:to_sym)
|
692
|
-
).send(opts[:menu_task_color].to_sym),
|
693
|
-
oname: format(
|
694
|
-
opts[:menu_task_format],
|
695
|
-
$~.named_captures.transform_keys(&:to_sym)
|
696
|
-
) }
|
697
|
-
)
|
698
|
-
end
|
699
|
-
else
|
700
|
-
# line not added
|
701
|
-
end
|
843
|
+
process_line_blocks(blocks, fcb, opts, use_chrome)
|
702
844
|
when :blocks
|
703
|
-
|
704
|
-
#
|
705
|
-
blocks.push get_block_summary(opts, fcb) ### if Filter.fcb_select? opts, fcb
|
845
|
+
append_block_summary(blocks, fcb, opts)
|
706
846
|
end
|
707
847
|
end
|
708
848
|
|
709
|
-
|
710
|
-
blocks.push FCB.new(
|
711
|
-
{ chrome: true,
|
712
|
-
disabled: '',
|
713
|
-
dname: format(opts[:menu_divider_format],
|
714
|
-
opts[:menu_final_divider])
|
715
|
-
.send(opts[:menu_divider_color].to_sym),
|
716
|
-
oname: opts[:menu_final_divider] }
|
717
|
-
)
|
718
|
-
end
|
849
|
+
append_final_divider(blocks, opts) if use_chrome
|
719
850
|
blocks
|
720
851
|
rescue StandardError => err
|
721
|
-
|
722
|
-
|
723
|
-
|
852
|
+
handle_error(err)
|
853
|
+
end
|
854
|
+
|
855
|
+
##
|
856
|
+
# Processes lines within the file and converts them into blocks if they match certain criteria.
|
857
|
+
#
|
858
|
+
def process_line_blocks(blocks, fcb, opts, use_chrome)
|
859
|
+
## convert line to block
|
860
|
+
#
|
861
|
+
if opts[:menu_divider_match].present? &&
|
862
|
+
(mbody = fcb.body[0].match opts[:menu_divider_match])
|
863
|
+
if use_chrome
|
864
|
+
blocks.push FCB.new(
|
865
|
+
{ chrome: true,
|
866
|
+
disabled: '',
|
867
|
+
dname: format(opts[:menu_divider_format],
|
868
|
+
mbody[:name]).send(opts[:menu_divider_color].to_sym),
|
869
|
+
oname: mbody[:name] }
|
870
|
+
)
|
871
|
+
end
|
872
|
+
elsif opts[:menu_task_match].present? &&
|
873
|
+
(fcb.body[0].match opts[:menu_task_match])
|
874
|
+
if use_chrome
|
875
|
+
blocks.push FCB.new(
|
876
|
+
{ chrome: true,
|
877
|
+
disabled: '',
|
878
|
+
dname: format(
|
879
|
+
opts[:menu_task_format],
|
880
|
+
$~.named_captures.transform_keys(&:to_sym)
|
881
|
+
).send(opts[:menu_task_color].to_sym),
|
882
|
+
oname: format(
|
883
|
+
opts[:menu_task_format],
|
884
|
+
$~.named_captures.transform_keys(&:to_sym)
|
885
|
+
) }
|
886
|
+
)
|
887
|
+
end
|
888
|
+
else
|
889
|
+
# line not added
|
890
|
+
end
|
724
891
|
end
|
725
892
|
|
726
893
|
def list_default_env
|
@@ -747,39 +914,22 @@ module MarkdownExec
|
|
747
914
|
|
748
915
|
def list_files_per_options(options)
|
749
916
|
list_files_specified(
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
917
|
+
determine_filename(
|
918
|
+
specified_filename: options[:filename]&.present? ? options[:filename] : nil,
|
919
|
+
specified_folder: options[:path],
|
920
|
+
default_filename: 'README.md',
|
921
|
+
default_folder: '.'
|
922
|
+
)
|
754
923
|
)
|
755
924
|
end
|
756
925
|
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
[specified_folder, specified_filename]
|
765
|
-
else
|
766
|
-
[default_folder, specified_filename]
|
767
|
-
end
|
768
|
-
elsif specified_folder&.present?
|
769
|
-
if filetree
|
770
|
-
[specified_folder, @options[:md_filename_match]]
|
771
|
-
else
|
772
|
-
[specified_folder, @options[:md_filename_glob]]
|
773
|
-
end
|
774
|
-
else
|
775
|
-
[default_folder, default_filename]
|
776
|
-
end)
|
777
|
-
if filetree
|
778
|
-
filetree.select do |filename|
|
779
|
-
filename == fn || filename.match(/^#{fn}$/) || filename.match(%r{^#{fn}/.+$})
|
780
|
-
end
|
781
|
-
else
|
782
|
-
Dir.glob(fn)
|
926
|
+
## Searches for files based on the specified or default filenames and folders
|
927
|
+
#
|
928
|
+
def list_files_specified(fn, filetree = nil)
|
929
|
+
return Dir.glob(fn) unless filetree
|
930
|
+
|
931
|
+
filetree.select do |filename|
|
932
|
+
filename == fn || filename.match(/^#{fn}$/) || filename.match(%r{^#{fn}/.+$})
|
783
933
|
end
|
784
934
|
end
|
785
935
|
|
@@ -788,17 +938,6 @@ module MarkdownExec
|
|
788
938
|
@options[:md_filename_glob]))
|
789
939
|
end
|
790
940
|
|
791
|
-
def blocks_per_opts(blocks, opts)
|
792
|
-
if opts[:struct]
|
793
|
-
blocks
|
794
|
-
else
|
795
|
-
# blocks.map(&:name)
|
796
|
-
blocks.map do |block|
|
797
|
-
block.fetch(:text, nil) || block.oname
|
798
|
-
end
|
799
|
-
end.compact.reject(&:empty?)
|
800
|
-
end
|
801
|
-
|
802
941
|
## output type (body string or full object) per option struct and bash
|
803
942
|
#
|
804
943
|
def list_named_blocks_in_file(call_options = {}, &options_block)
|
@@ -811,6 +950,17 @@ module MarkdownExec
|
|
811
950
|
blocks_per_opts(blocks, opts)
|
812
951
|
end
|
813
952
|
|
953
|
+
## Handles the file loading and returns the blocks in the file and MDoc instance
|
954
|
+
#
|
955
|
+
def load_file_and_prepare_menu(opts)
|
956
|
+
blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
|
957
|
+
mdoc = MDoc.new(blocks_in_file) do |nopts|
|
958
|
+
opts.merge!(nopts)
|
959
|
+
end
|
960
|
+
blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
|
961
|
+
[blocks_in_file, blocks_menu, mdoc]
|
962
|
+
end
|
963
|
+
|
814
964
|
def make_block_labels(call_options = {})
|
815
965
|
opts = options.merge(call_options)
|
816
966
|
list_blocks_in_file(opts).map do |fcb|
|
@@ -826,6 +976,32 @@ module MarkdownExec
|
|
826
976
|
end.compact
|
827
977
|
end
|
828
978
|
|
979
|
+
def menu_export(data = menu_for_optparse)
|
980
|
+
data.map do |item|
|
981
|
+
item.delete(:procname)
|
982
|
+
item
|
983
|
+
end.to_yaml
|
984
|
+
end
|
985
|
+
|
986
|
+
def menu_for_blocks(menu_options)
|
987
|
+
options = calculated_options.merge menu_options
|
988
|
+
menu = []
|
989
|
+
iter_blocks_in_file(options) do |btype, fcb|
|
990
|
+
case btype
|
991
|
+
when :filter
|
992
|
+
%i[blocks line]
|
993
|
+
when :line
|
994
|
+
if options[:menu_divider_match] &&
|
995
|
+
(mbody = fcb.body[0].match(options[:menu_divider_match]))
|
996
|
+
menu.push FCB.new({ dname: mbody[:name], oname: mbody[:name], disabled: '' })
|
997
|
+
end
|
998
|
+
when :blocks
|
999
|
+
menu += [fcb.oname]
|
1000
|
+
end
|
1001
|
+
end
|
1002
|
+
menu
|
1003
|
+
end
|
1004
|
+
|
829
1005
|
# :reek:DuplicateMethodCall
|
830
1006
|
# :reek:NestedIterators
|
831
1007
|
def menu_for_optparse
|
@@ -849,11 +1025,11 @@ module MarkdownExec
|
|
849
1025
|
}
|
850
1026
|
when 'path'
|
851
1027
|
lambda { |value|
|
852
|
-
read_configuration_file!
|
1028
|
+
read_configuration_file!(options, value)
|
853
1029
|
}
|
854
1030
|
when 'show_config'
|
855
1031
|
lambda { |_|
|
856
|
-
|
1032
|
+
finalize_cli_argument_processing(options)
|
857
1033
|
fout options.sort_by_key.to_yaml
|
858
1034
|
}
|
859
1035
|
when 'val_as_bool'
|
@@ -877,33 +1053,14 @@ module MarkdownExec
|
|
877
1053
|
end
|
878
1054
|
end
|
879
1055
|
|
880
|
-
def
|
881
|
-
|
882
|
-
menu = []
|
883
|
-
iter_blocks_in_file(options) do |btype, fcb|
|
884
|
-
case btype
|
885
|
-
when :filter
|
886
|
-
%i[blocks line]
|
887
|
-
when :line
|
888
|
-
if options[:menu_divider_match] &&
|
889
|
-
(mbody = fcb.body[0].match(options[:menu_divider_match]))
|
890
|
-
menu.push FCB.new({ dname: mbody[:name], oname: mbody[:name], disabled: '' })
|
891
|
-
end
|
892
|
-
when :blocks
|
893
|
-
menu += [fcb.oname]
|
894
|
-
end
|
895
|
-
end
|
896
|
-
menu
|
1056
|
+
def menu_help
|
1057
|
+
@option_parser.help
|
897
1058
|
end
|
898
1059
|
|
899
1060
|
def menu_iter(data = menu_for_optparse, &block)
|
900
1061
|
data.map(&block)
|
901
1062
|
end
|
902
1063
|
|
903
|
-
def menu_help
|
904
|
-
@option_parser.help
|
905
|
-
end
|
906
|
-
|
907
1064
|
def menu_option_append(opts, options, item)
|
908
1065
|
return unless item[:long_name].present? || item[:short_name].present?
|
909
1066
|
|
@@ -931,27 +1088,6 @@ module MarkdownExec
|
|
931
1088
|
].compact)
|
932
1089
|
end
|
933
1090
|
|
934
|
-
## post-parse options configuration
|
935
|
-
#
|
936
|
-
def options_finalize(rest)
|
937
|
-
## position 0: file or folder (optional)
|
938
|
-
#
|
939
|
-
if (pos = rest.shift)&.present?
|
940
|
-
if Dir.exist?(pos)
|
941
|
-
@options[:path] = pos
|
942
|
-
elsif File.exist?(pos)
|
943
|
-
@options[:filename] = pos
|
944
|
-
else
|
945
|
-
raise FileMissingError, pos, caller
|
946
|
-
end
|
947
|
-
end
|
948
|
-
|
949
|
-
## position 1: block name (optional)
|
950
|
-
#
|
951
|
-
block_name = rest.shift
|
952
|
-
@options[:block_name] = block_name if block_name.present?
|
953
|
-
end
|
954
|
-
|
955
1091
|
# :reek:ControlParameter
|
956
1092
|
def optsmerge(call_options = {}, options_block = nil)
|
957
1093
|
class_call_options = @options.merge(call_options || {})
|
@@ -995,63 +1131,112 @@ module MarkdownExec
|
|
995
1131
|
}
|
996
1132
|
end
|
997
1133
|
|
998
|
-
|
999
|
-
#
|
1000
|
-
## Adds a back option at the head or tail of a menu
|
1134
|
+
# Prepare the blocks menu by adding labels and other necessary details.
|
1001
1135
|
#
|
1002
|
-
|
1003
|
-
|
1136
|
+
# @param blocks_in_file [Array<Hash>] The list of blocks from the file.
|
1137
|
+
# @param opts [Hash] The options hash.
|
1138
|
+
# @return [Array<Hash>] The updated blocks menu.
|
1139
|
+
def prepare_blocks_menu(blocks_in_file, opts)
|
1140
|
+
# next if fcb.fetch(:disabled, false)
|
1141
|
+
# next unless fcb.fetch(:name, '').present?
|
1142
|
+
blocks_in_file.map do |fcb|
|
1143
|
+
fcb.merge!(
|
1144
|
+
name: fcb.dname,
|
1145
|
+
label: BlockLabel.make(
|
1146
|
+
body: fcb[:body],
|
1147
|
+
filename: opts[:filename],
|
1148
|
+
headings: fcb.fetch(:headings, []),
|
1149
|
+
menu_blocks_with_docname: opts[:menu_blocks_with_docname],
|
1150
|
+
menu_blocks_with_headings: opts[:menu_blocks_with_headings],
|
1151
|
+
text: fcb[:text],
|
1152
|
+
title: fcb[:title]
|
1153
|
+
)
|
1154
|
+
)
|
1155
|
+
fcb.to_h
|
1156
|
+
end.compact
|
1157
|
+
end
|
1004
1158
|
|
1005
|
-
|
1006
|
-
|
1159
|
+
# Prepares and fetches file listings
|
1160
|
+
def prepare_file_list(options)
|
1161
|
+
list_files_per_options(options)
|
1162
|
+
end
|
1007
1163
|
|
1008
|
-
|
1009
|
-
|
1010
|
-
|
1011
|
-
)
|
1164
|
+
def process_fenced_block(fcb, opts, selected_messages, &block)
|
1165
|
+
fcb.oname = fcb.dname = fcb.title || ''
|
1166
|
+
return unless fcb.body
|
1012
1167
|
|
1013
|
-
|
1168
|
+
set_fcb_title(fcb)
|
1169
|
+
|
1170
|
+
if block &&
|
1171
|
+
selected_messages.include?(:blocks) &&
|
1172
|
+
Filter.fcb_select?(opts, fcb)
|
1173
|
+
block.call :blocks, fcb
|
1174
|
+
end
|
1014
1175
|
end
|
1015
1176
|
|
1016
|
-
|
1177
|
+
def process_line(line, _opts, selected_messages, &block)
|
1178
|
+
return unless block && selected_messages.include?(:line)
|
1179
|
+
|
1180
|
+
# dp 'text outside of fcb'
|
1181
|
+
fcb = FCB.new
|
1182
|
+
fcb.body = [line]
|
1183
|
+
block.call(:line, fcb)
|
1184
|
+
end
|
1185
|
+
|
1186
|
+
##
|
1187
|
+
# Presents a menu to the user for approving an action and performs additional tasks based on the selection.
|
1188
|
+
# The function provides options for approval, rejection, copying data to clipboard, or saving data to a file.
|
1017
1189
|
#
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
|
1190
|
+
# @param opts [Hash] A hash containing various options for the menu.
|
1191
|
+
# @param required_lines [Array<String>] Lines of text or code that are subject to user approval.
|
1192
|
+
#
|
1193
|
+
# @option opts [String] :prompt_approve_block Prompt text for the approval menu.
|
1194
|
+
# @option opts [String] :prompt_yes Text for the 'Yes' choice in the menu.
|
1195
|
+
# @option opts [String] :prompt_no Text for the 'No' choice in the menu.
|
1196
|
+
# @option opts [String] :prompt_script_to_clipboard Text for the 'Copy to Clipboard' choice in the menu.
|
1197
|
+
# @option opts [String] :prompt_save_script Text for the 'Save to File' choice in the menu.
|
1198
|
+
#
|
1199
|
+
# @return [Boolean] Returns true if the user approves (selects 'Yes'), false otherwise.
|
1200
|
+
##
|
1201
|
+
def prompt_for_user_approval(opts, required_lines)
|
1202
|
+
# Present a selection menu for user approval.
|
1203
|
+
sel = @prompt.select(opts[:prompt_approve_block], filter: true) do |menu|
|
1204
|
+
menu.default 1
|
1205
|
+
menu.choice opts[:prompt_yes], 1
|
1206
|
+
menu.choice opts[:prompt_no], 2
|
1207
|
+
menu.choice opts[:prompt_script_to_clipboard], 3
|
1208
|
+
menu.choice opts[:prompt_save_script], 4
|
1023
1209
|
end
|
1210
|
+
|
1211
|
+
if sel == 3
|
1212
|
+
copy_to_clipboard(required_lines)
|
1213
|
+
elsif sel == 4
|
1214
|
+
save_to_file(opts, required_lines)
|
1215
|
+
end
|
1216
|
+
|
1217
|
+
sel == 1
|
1024
1218
|
end
|
1025
1219
|
|
1026
|
-
##
|
1027
|
-
# insert exit option at head or tail
|
1028
|
-
# return selected option or nil
|
1220
|
+
## insert back option at head or tail
|
1029
1221
|
#
|
1030
|
-
|
1031
|
-
|
1032
|
-
|
1033
|
-
|
1034
|
-
|
1035
|
-
|
1036
|
-
|
1222
|
+
## Adds a back option at the head or tail of a menu
|
1223
|
+
#
|
1224
|
+
def prompt_menu_add_back(items, label)
|
1225
|
+
return items unless @options[:menu_with_back] && history_state_exist?
|
1226
|
+
|
1227
|
+
state = history_state_partition(@options)
|
1228
|
+
@hs_curr = state[:unit]
|
1229
|
+
@hs_rest = state[:rest]
|
1230
|
+
@options[:menu_back_at_top] ? [label] + items : items + [label]
|
1037
1231
|
end
|
1038
1232
|
|
1039
|
-
##
|
1040
|
-
# insert exit option at head or tail
|
1041
|
-
# return option:, selected option:
|
1233
|
+
## insert exit option at head or tail
|
1042
1234
|
#
|
1043
|
-
def
|
1044
|
-
|
1045
|
-
|
1046
|
-
prompt_menu_add_back(items)
|
1047
|
-
),
|
1048
|
-
opts.merge(filter: true))
|
1049
|
-
if sel == BACK_OPTION
|
1050
|
-
{ option: sel, curr: @hs_curr, rest: @hs_rest }
|
1051
|
-
elsif sel == EXIT_OPTION
|
1052
|
-
{ option: sel }
|
1235
|
+
def prompt_menu_add_exit(items, label)
|
1236
|
+
if @options[:menu_exit_at_top]
|
1237
|
+
(@options[:menu_with_exit] ? [label] : []) + items
|
1053
1238
|
else
|
1054
|
-
|
1239
|
+
items + (@options[:menu_with_exit] ? [label] : [])
|
1055
1240
|
end
|
1056
1241
|
end
|
1057
1242
|
|
@@ -1063,54 +1248,33 @@ module MarkdownExec
|
|
1063
1248
|
.transform_keys(&:to_sym))
|
1064
1249
|
end
|
1065
1250
|
|
1066
|
-
#
|
1067
|
-
|
1068
|
-
|
1069
|
-
|
1070
|
-
|
1071
|
-
|
1251
|
+
# Reads required code blocks from a temporary file specified by an environment variable.
|
1252
|
+
#
|
1253
|
+
# @return [Array<String>] An array containing the lines read from the temporary file.
|
1254
|
+
# @note Relies on the 'MDE_LINK_REQUIRED_FILE' environment variable to locate the file.
|
1255
|
+
def read_required_blocks_from_temp_file
|
1256
|
+
temp_blocks = []
|
1072
1257
|
|
1073
|
-
|
1074
|
-
|
1075
|
-
opts.banner = [
|
1076
|
-
"#{MarkdownExec::APP_NAME}" \
|
1077
|
-
" - #{MarkdownExec::APP_DESC} (#{MarkdownExec::VERSION})",
|
1078
|
-
"Usage: #{executable_name} [(path | filename [block_name])] [options]"
|
1079
|
-
].join("\n")
|
1258
|
+
temp_blocks_file_path = ENV.fetch('MDE_LINK_REQUIRED_FILE', nil)
|
1259
|
+
return temp_blocks if temp_blocks_file_path.nil? || temp_blocks_file_path.empty?
|
1080
1260
|
|
1081
|
-
|
1082
|
-
|
1083
|
-
end
|
1261
|
+
if File.exist?(temp_blocks_file_path)
|
1262
|
+
temp_blocks = File.readlines(temp_blocks_file_path, chomp: true)
|
1084
1263
|
end
|
1085
|
-
option_parser.load
|
1086
|
-
option_parser.environment
|
1087
|
-
rest = option_parser.parse!(arguments_for_mde) # (into: options)
|
1088
1264
|
|
1089
|
-
|
1090
|
-
|
1265
|
+
temp_blocks
|
1266
|
+
end
|
1091
1267
|
|
1092
|
-
|
1093
|
-
|
1094
|
-
|
1095
|
-
|
1096
|
-
puts "File missing: #{err}"
|
1097
|
-
end
|
1268
|
+
def run
|
1269
|
+
clear_required_file
|
1270
|
+
execute_block_with_error_handling(initialize_and_parse_cli_options)
|
1271
|
+
delete_required_temp_file
|
1098
1272
|
rescue StandardError => err
|
1099
1273
|
warn(error = "ERROR ** MarkParse.run(); #{err.inspect}")
|
1100
1274
|
binding.pry if $tap_enable
|
1101
1275
|
raise ArgumentError, error
|
1102
1276
|
end
|
1103
1277
|
|
1104
|
-
def saved_name_split(name)
|
1105
|
-
# rubocop:disable Layout/LineLength
|
1106
|
-
mf = /#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_,_(?<block>.+)\.sh/.match name
|
1107
|
-
# rubocop:enable Layout/LineLength
|
1108
|
-
return unless mf
|
1109
|
-
|
1110
|
-
@options[:block_name] = mf[:block]
|
1111
|
-
@options[:filename] = mf[:file].gsub(FNR12, FNR11)
|
1112
|
-
end
|
1113
|
-
|
1114
1278
|
def run_last_script
|
1115
1279
|
filename = SavedFilesMatcher.most_recent(@options[:saved_script_folder],
|
1116
1280
|
@options[:saved_script_glob])
|
@@ -1121,55 +1285,20 @@ module MarkdownExec
|
|
1121
1285
|
select_approve_and_execute_block({})
|
1122
1286
|
end
|
1123
1287
|
|
1124
|
-
def
|
1125
|
-
|
1126
|
-
|
1127
|
-
@options[:
|
1128
|
-
SavedAsset.stdout_name(blockname: @options[:block_name],
|
1129
|
-
filename: File.basename(@options[:filename], '.*'),
|
1130
|
-
prefix: @options[:logged_stdout_filename_prefix],
|
1131
|
-
time: Time.now.utc)
|
1132
|
-
|
1133
|
-
@options[:logged_stdout_filespec] =
|
1134
|
-
File.join @options[:saved_stdout_folder],
|
1135
|
-
@options[:logged_stdout_filename]
|
1136
|
-
@logged_stdout_filespec = @options[:logged_stdout_filespec]
|
1137
|
-
(dirname = File.dirname(@options[:logged_stdout_filespec]))
|
1138
|
-
FileUtils.mkdir_p dirname
|
1139
|
-
|
1140
|
-
ol = ["-STDOUT-\n"]
|
1141
|
-
ol += @execute_files&.fetch(EF_STDOUT, [])
|
1142
|
-
ol += ["\n-STDERR-\n"]
|
1143
|
-
ol += @execute_files&.fetch(EF_STDERR, [])
|
1144
|
-
ol += ["\n-STDIN-\n"]
|
1145
|
-
ol += @execute_files&.fetch(EF_STDIN, [])
|
1146
|
-
ol += ["\n"]
|
1147
|
-
File.write(@options[:logged_stdout_filespec], ol.join)
|
1288
|
+
def save_to_file(opts, required_lines)
|
1289
|
+
write_command_file(opts.merge(save_executed_script: true),
|
1290
|
+
required_lines)
|
1291
|
+
fout "File saved: #{@options[:saved_filespec]}"
|
1148
1292
|
end
|
1149
1293
|
|
1150
|
-
|
1151
|
-
|
1152
|
-
|
1153
|
-
|
1154
|
-
|
1155
|
-
|
1156
|
-
|
1157
|
-
|
1158
|
-
blocks_in_file.map do |fcb|
|
1159
|
-
fcb.merge!(
|
1160
|
-
name: fcb.dname,
|
1161
|
-
label: BlockLabel.make(
|
1162
|
-
body: fcb[:body],
|
1163
|
-
filename: opts[:filename],
|
1164
|
-
headings: fcb.fetch(:headings, []),
|
1165
|
-
menu_blocks_with_docname: opts[:menu_blocks_with_docname],
|
1166
|
-
menu_blocks_with_headings: opts[:menu_blocks_with_headings],
|
1167
|
-
text: fcb[:text],
|
1168
|
-
title: fcb[:title]
|
1169
|
-
)
|
1170
|
-
)
|
1171
|
-
fcb.to_h
|
1172
|
-
end.compact
|
1294
|
+
def saved_name_split(name)
|
1295
|
+
# rubocop:disable Layout/LineLength
|
1296
|
+
mf = /#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_,_(?<block>.+)\.sh/.match name
|
1297
|
+
# rubocop:enable Layout/LineLength
|
1298
|
+
return unless mf
|
1299
|
+
|
1300
|
+
@options[:block_name] = mf[:block]
|
1301
|
+
@options[:filename] = mf[:file].gsub(FNR12, FNR11)
|
1173
1302
|
end
|
1174
1303
|
|
1175
1304
|
# Select and execute a code block from a Markdown document.
|
@@ -1183,59 +1312,33 @@ module MarkdownExec
|
|
1183
1312
|
def select_approve_and_execute_block(call_options, &options_block)
|
1184
1313
|
opts = optsmerge(call_options, options_block)
|
1185
1314
|
repeat_menu = true && !opts[:block_name].present?
|
1186
|
-
|
1187
1315
|
load_file = !LOAD_FILE
|
1316
|
+
default = 1
|
1317
|
+
|
1188
1318
|
loop do
|
1189
|
-
# load file
|
1190
|
-
#
|
1191
1319
|
loop do
|
1192
|
-
|
1193
|
-
|
1194
|
-
|
1195
|
-
blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
|
1196
|
-
mdoc = MDoc.new(blocks_in_file) do |nopts|
|
1197
|
-
opts.merge!(nopts)
|
1198
|
-
end
|
1199
|
-
blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
|
1320
|
+
opts[:back] = false
|
1321
|
+
blocks_in_file, blocks_menu, mdoc = load_file_and_prepare_menu(opts)
|
1322
|
+
|
1200
1323
|
unless opts[:block_name].present?
|
1201
|
-
|
1202
|
-
|
1203
|
-
|
1204
|
-
|
1205
|
-
# sel = prompt_with_quit(pt, bm, per_page: opts[:select_page_height])
|
1206
|
-
# return nil if sel.nil?
|
1207
|
-
obj = prompt_with_quit2(pt, bm, per_page: opts[:select_page_height])
|
1208
|
-
case obj.fetch(:option, nil)
|
1209
|
-
when EXIT_OPTION
|
1324
|
+
block_name, state = wait_for_user_selection(blocks_in_file, blocks_menu, default,
|
1325
|
+
opts)
|
1326
|
+
case state
|
1327
|
+
when :exit
|
1210
1328
|
return nil
|
1211
|
-
when
|
1212
|
-
opts[:
|
1213
|
-
opts[:
|
1214
|
-
|
1215
|
-
|
1216
|
-
else
|
1217
|
-
sel = obj[:selected]
|
1218
|
-
|
1219
|
-
## store selected option
|
1220
|
-
#
|
1221
|
-
label_block = blocks_in_file.select do |fcb|
|
1222
|
-
fcb.dname == sel
|
1223
|
-
end.fetch(0, nil)
|
1224
|
-
opts[:block_name] = @options[:block_name] = label_block.oname
|
1329
|
+
when :back
|
1330
|
+
opts[:block_name] = block_name[:option]
|
1331
|
+
opts[:back] = true
|
1332
|
+
when :continue
|
1333
|
+
opts[:block_name] = block_name
|
1225
1334
|
end
|
1226
1335
|
end
|
1227
|
-
break if load_file == LOAD_FILE
|
1228
1336
|
|
1229
|
-
|
1230
|
-
|
1231
|
-
|
1232
|
-
|
1233
|
-
opts[:block_name] = block_name
|
1234
|
-
if load_file == LOAD_FILE
|
1235
|
-
repeat_menu = true
|
1236
|
-
break
|
1237
|
-
end
|
1337
|
+
load_file, next_block_name = approve_and_execute_block(opts, mdoc)
|
1338
|
+
default = load_file == LOAD_FILE ? 1 : opts[:block_name]
|
1339
|
+
opts[:block_name] = next_block_name
|
1238
1340
|
|
1341
|
+
break if state == :continue && load_file == LOAD_FILE
|
1239
1342
|
break unless repeat_menu
|
1240
1343
|
end
|
1241
1344
|
break if load_file != LOAD_FILE
|
@@ -1247,25 +1350,53 @@ module MarkdownExec
|
|
1247
1350
|
raise ArgumentError, error
|
1248
1351
|
end
|
1249
1352
|
|
1250
|
-
def
|
1251
|
-
|
1252
|
-
|
1253
|
-
|
1254
|
-
|
1255
|
-
|
1256
|
-
|
1353
|
+
def select_document_if_multiple(files = list_markdown_files_in_path)
|
1354
|
+
return files[0] if (count = files.count) == 1
|
1355
|
+
|
1356
|
+
return unless count >= 2
|
1357
|
+
|
1358
|
+
opts = options.dup
|
1359
|
+
select_option_or_exit opts[:prompt_select_md].to_s, files,
|
1360
|
+
opts.merge(per_page: opts[:select_page_height])
|
1361
|
+
end
|
1362
|
+
|
1363
|
+
# Presents a TTY prompt to select an option or exit, returns selected option or nil
|
1364
|
+
def select_option_or_exit(prompt_text, items, opts = {})
|
1365
|
+
result = select_option_with_metadata(prompt_text, items, opts)
|
1366
|
+
return unless result.fetch(:option, nil)
|
1367
|
+
|
1368
|
+
result[:selected]
|
1369
|
+
end
|
1370
|
+
|
1371
|
+
# Presents a TTY prompt to select an option or exit, returns metadata including option and selected
|
1372
|
+
def select_option_with_metadata(prompt_text, items, opts = {})
|
1373
|
+
selection = @prompt.select(prompt_text,
|
1374
|
+
prompt_menu_add_exit(
|
1375
|
+
prompt_menu_add_back(
|
1376
|
+
items,
|
1377
|
+
opts[:menu_option_back_name]
|
1378
|
+
),
|
1379
|
+
opts[:menu_option_exit_name]
|
1380
|
+
),
|
1381
|
+
opts.merge(filter: true))
|
1382
|
+
if selection == opts[:menu_option_back_name]
|
1383
|
+
{ option: selection, curr: @hs_curr, rest: @hs_rest, shell: BLOCK_TYPE_LINK }
|
1384
|
+
elsif selection == opts[:menu_option_exit_name]
|
1385
|
+
{ option: selection }
|
1386
|
+
else
|
1387
|
+
{ selected: selection }
|
1257
1388
|
end
|
1258
1389
|
end
|
1259
1390
|
|
1260
1391
|
def select_recent_output
|
1261
|
-
filename =
|
1392
|
+
filename = select_option_or_exit(
|
1262
1393
|
@options[:prompt_select_output].to_s,
|
1263
1394
|
list_recent_output(
|
1264
1395
|
@options[:saved_stdout_folder],
|
1265
1396
|
@options[:saved_stdout_glob],
|
1266
1397
|
@options[:list_count]
|
1267
1398
|
),
|
1268
|
-
{ per_page: @options[:select_page_height] }
|
1399
|
+
@options.merge({ per_page: @options[:select_page_height] })
|
1269
1400
|
)
|
1270
1401
|
return unless filename.present?
|
1271
1402
|
|
@@ -1273,14 +1404,14 @@ module MarkdownExec
|
|
1273
1404
|
end
|
1274
1405
|
|
1275
1406
|
def select_recent_script
|
1276
|
-
filename =
|
1407
|
+
filename = select_option_or_exit(
|
1277
1408
|
@options[:prompt_select_md].to_s,
|
1278
1409
|
list_recent_scripts(
|
1279
1410
|
@options[:saved_script_folder],
|
1280
1411
|
@options[:saved_script_glob],
|
1281
1412
|
@options[:list_count]
|
1282
1413
|
),
|
1283
|
-
{ per_page: @options[:select_page_height] }
|
1414
|
+
@options.merge({ per_page: @options[:select_page_height] })
|
1284
1415
|
)
|
1285
1416
|
return if filename.nil?
|
1286
1417
|
|
@@ -1293,27 +1424,37 @@ module MarkdownExec
|
|
1293
1424
|
})
|
1294
1425
|
end
|
1295
1426
|
|
1296
|
-
|
1297
|
-
|
1298
|
-
|
1299
|
-
|
1300
|
-
|
1427
|
+
# set the title of an FCB object based on its body if it is nil or empty
|
1428
|
+
def set_fcb_title(fcb)
|
1429
|
+
return unless fcb.title.nil? || fcb.title.empty?
|
1430
|
+
|
1431
|
+
fcb.title = (fcb&.body || []).join(' ').gsub(/ +/, ' ')[0..64]
|
1301
1432
|
end
|
1302
1433
|
|
1303
|
-
|
1304
|
-
|
1305
|
-
|
1306
|
-
|
1307
|
-
|
1308
|
-
|
1309
|
-
|
1310
|
-
|
1311
|
-
|
1312
|
-
|
1313
|
-
|
1314
|
-
|
1315
|
-
|
1434
|
+
def start_fenced_block(opts, line, headings, fenced_start_extended_regex)
|
1435
|
+
fcb_title_groups = line.match(fenced_start_extended_regex).named_captures.sym_keys
|
1436
|
+
rest = fcb_title_groups.fetch(:rest, '')
|
1437
|
+
|
1438
|
+
fcb = FCB.new
|
1439
|
+
fcb.headings = headings
|
1440
|
+
fcb.oname = fcb.dname = fcb_title_groups.fetch(:name, '')
|
1441
|
+
fcb.shell = fcb_title_groups.fetch(:shell, '')
|
1442
|
+
fcb.title = fcb_title_groups.fetch(:name, '')
|
1443
|
+
fcb.body = []
|
1444
|
+
fcb.reqs, fcb.wraps =
|
1445
|
+
ArrayUtil.partition_by_predicate(rest.scan(/\+[^\s]+/).map do |req|
|
1446
|
+
req[1..-1]
|
1447
|
+
end) do |name|
|
1448
|
+
!name.match(Regexp.new(opts[:block_name_wrapper_match]))
|
1316
1449
|
end
|
1450
|
+
fcb.call = rest.match(Regexp.new(opts[:block_calls_scan]))&.to_a&.first
|
1451
|
+
fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
|
1452
|
+
tn.named_captures.sym_keys
|
1453
|
+
end
|
1454
|
+
fcb.stdout = if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
|
1455
|
+
tn.named_captures.sym_keys
|
1456
|
+
end
|
1457
|
+
fcb
|
1317
1458
|
end
|
1318
1459
|
|
1319
1460
|
def tab_completions(data = menu_for_optparse)
|
@@ -1322,6 +1463,77 @@ module MarkdownExec
|
|
1322
1463
|
end.compact
|
1323
1464
|
end
|
1324
1465
|
|
1466
|
+
def tty_prompt_without_disabled_symbol
|
1467
|
+
TTY::Prompt.new(interrupt: :exit, symbols: { cross: ' ' })
|
1468
|
+
end
|
1469
|
+
|
1470
|
+
##
|
1471
|
+
# Updates the hierarchy of document headings based on the given line and existing headings.
|
1472
|
+
# The function uses regular expressions specified in the `opts` to identify different levels of headings.
|
1473
|
+
#
|
1474
|
+
# @param line [String] The line of text to examine for heading content.
|
1475
|
+
# @param headings [Array<String>] The existing list of document headings.
|
1476
|
+
# @param opts [Hash] A hash containing options for regular expression matches for different heading levels.
|
1477
|
+
#
|
1478
|
+
# @option opts [String] :heading1_match Regular expression for matching first-level headings.
|
1479
|
+
# @option opts [String] :heading2_match Regular expression for matching second-level headings.
|
1480
|
+
# @option opts [String] :heading3_match Regular expression for matching third-level headings.
|
1481
|
+
#
|
1482
|
+
# @return [Array<String>] Updated list of headings.
|
1483
|
+
def update_document_headings(line, headings, opts)
|
1484
|
+
if (lm = line.match(Regexp.new(opts[:heading3_match])))
|
1485
|
+
[headings[0], headings[1], lm[:name]]
|
1486
|
+
elsif (lm = line.match(Regexp.new(opts[:heading2_match])))
|
1487
|
+
[headings[0], lm[:name]]
|
1488
|
+
elsif (lm = line.match(Regexp.new(opts[:heading1_match])))
|
1489
|
+
[lm[:name]]
|
1490
|
+
else
|
1491
|
+
headings
|
1492
|
+
end
|
1493
|
+
end
|
1494
|
+
|
1495
|
+
##
|
1496
|
+
# Processes an individual line within a loop, updating headings and handling fenced code blocks.
|
1497
|
+
# This function is designed to be called within a loop that iterates through each line of a document.
|
1498
|
+
#
|
1499
|
+
# @param line [String] The current line being processed.
|
1500
|
+
# @param state [Hash] The current state of the parser, including flags and data related to the processing.
|
1501
|
+
# @param opts [Hash] A hash containing various options for line and block processing.
|
1502
|
+
# @param selected_messages [Array<String>] Accumulator for lines or messages that are subject to further processing.
|
1503
|
+
# @param block [Proc] An optional block for further processing or transformation of lines.
|
1504
|
+
#
|
1505
|
+
# @option state [Array<String>] :headings Current headings to be updated based on the line.
|
1506
|
+
# @option state [Regexp] :fenced_start_and_end_regex Regular expression to match the start and end of a fenced block.
|
1507
|
+
# @option state [Boolean] :in_fenced_block Flag indicating whether the current line is inside a fenced block.
|
1508
|
+
# @option state [Object] :fcb An object representing the current fenced code block being processed.
|
1509
|
+
#
|
1510
|
+
# @option opts [Boolean] :menu_blocks_with_headings Flag indicating whether to update headings while processing.
|
1511
|
+
#
|
1512
|
+
# @return [Void] The function modifies the `state` and `selected_messages` arguments in place.
|
1513
|
+
##
|
1514
|
+
def update_line_and_block_state(line, state, opts, selected_messages, &block)
|
1515
|
+
if opts[:menu_blocks_with_headings]
|
1516
|
+
state[:headings] =
|
1517
|
+
update_document_headings(line, state[:headings], opts)
|
1518
|
+
end
|
1519
|
+
|
1520
|
+
if line.match(state[:fenced_start_and_end_regex])
|
1521
|
+
if state[:in_fenced_block]
|
1522
|
+
process_fenced_block(state[:fcb], opts, selected_messages, &block)
|
1523
|
+
state[:in_fenced_block] = false
|
1524
|
+
else
|
1525
|
+
state[:fcb] =
|
1526
|
+
start_fenced_block(opts, line, state[:headings],
|
1527
|
+
state[:fenced_start_extended_regex])
|
1528
|
+
state[:in_fenced_block] = true
|
1529
|
+
end
|
1530
|
+
elsif state[:in_fenced_block] && state[:fcb].body
|
1531
|
+
state[:fcb].body += [line.chomp]
|
1532
|
+
else
|
1533
|
+
process_line(line, opts, selected_messages, &block)
|
1534
|
+
end
|
1535
|
+
end
|
1536
|
+
|
1325
1537
|
# :reek:BooleanParameter
|
1326
1538
|
# :reek:ControlParameter
|
1327
1539
|
def update_options(opts = {}, over: true)
|
@@ -1333,6 +1545,29 @@ module MarkdownExec
|
|
1333
1545
|
@options
|
1334
1546
|
end
|
1335
1547
|
|
1548
|
+
## Handles the menu interaction and returns selected block name and option state
|
1549
|
+
#
|
1550
|
+
def wait_for_user_selection(blocks_in_file, blocks_menu, default, opts)
|
1551
|
+
pt = opts[:prompt_select_block].to_s
|
1552
|
+
bm = prepare_blocks_menu(blocks_menu, opts)
|
1553
|
+
return [nil, :exit] if bm.count.zero?
|
1554
|
+
|
1555
|
+
obj = select_option_with_metadata(pt, bm, opts.merge(
|
1556
|
+
default: default,
|
1557
|
+
per_page: opts[:select_page_height]
|
1558
|
+
))
|
1559
|
+
case obj.fetch(:option, nil)
|
1560
|
+
when opts[:menu_option_exit_name]
|
1561
|
+
[nil, :exit]
|
1562
|
+
when opts[:menu_option_back_name]
|
1563
|
+
[obj, :back]
|
1564
|
+
else
|
1565
|
+
label_block = blocks_in_file.find { |fcb| fcb.dname == obj[:selected] }
|
1566
|
+
[label_block.oname, :continue]
|
1567
|
+
end
|
1568
|
+
end
|
1569
|
+
|
1570
|
+
# Handles the core logic for generating the command file's metadata and content.
|
1336
1571
|
def write_command_file(call_options, required_lines)
|
1337
1572
|
return unless call_options[:save_executed_script]
|
1338
1573
|
|
@@ -1348,22 +1583,52 @@ module MarkdownExec
|
|
1348
1583
|
@options[:saved_filespec] =
|
1349
1584
|
File.join opts[:saved_script_folder], opts[:saved_script_filename]
|
1350
1585
|
|
1351
|
-
dirname = File.dirname(@options[:saved_filespec])
|
1352
|
-
FileUtils.mkdir_p dirname
|
1353
1586
|
shebang = if @options[:shebang]&.present?
|
1354
1587
|
"#{@options[:shebang]} #{@options[:shell]}\n"
|
1355
1588
|
else
|
1356
1589
|
''
|
1357
1590
|
end
|
1358
1591
|
|
1359
|
-
|
1360
|
-
|
1361
|
-
|
1362
|
-
|
1363
|
-
|
1364
|
-
return if @options[:saved_script_chmod].zero?
|
1592
|
+
content = shebang +
|
1593
|
+
"# file_name: #{opts[:filename]}\n" \
|
1594
|
+
"# block_name: #{opts[:block_name]}\n" \
|
1595
|
+
"# time: #{time_now}\n" \
|
1596
|
+
"#{required_lines.flatten.join("\n")}\n"
|
1365
1597
|
|
1366
|
-
|
1598
|
+
create_and_write_file_with_permissions(@options[:saved_filespec], content,
|
1599
|
+
@options[:saved_script_chmod])
|
1600
|
+
end
|
1601
|
+
|
1602
|
+
def write_execution_output_to_file
|
1603
|
+
FileUtils.mkdir_p File.dirname(@options[:logged_stdout_filespec])
|
1604
|
+
|
1605
|
+
ol = ["-STDOUT-\n"]
|
1606
|
+
ol += @execute_files&.fetch(EF_STDOUT, [])
|
1607
|
+
ol += ["\n-STDERR-\n"]
|
1608
|
+
ol += @execute_files&.fetch(EF_STDERR, [])
|
1609
|
+
ol += ["\n-STDIN-\n"]
|
1610
|
+
ol += @execute_files&.fetch(EF_STDIN, [])
|
1611
|
+
ol += ["\n"]
|
1612
|
+
File.write(@options[:logged_stdout_filespec], ol.join)
|
1613
|
+
end
|
1614
|
+
|
1615
|
+
# Writes required code blocks to a temporary file and sets an environment variable with its path.
|
1616
|
+
#
|
1617
|
+
# @param block_name [String] The name of the block to collect code for.
|
1618
|
+
# @param opts [Hash] Additional options for collecting code.
|
1619
|
+
# @note Sets the 'MDE_LINK_REQUIRED_FILE' environment variable to the temporary file path.
|
1620
|
+
def write_required_blocks_to_temp_file(mdoc, block_name, opts = {})
|
1621
|
+
code_blocks = (read_required_blocks_from_temp_file +
|
1622
|
+
mdoc.collect_recursively_required_code(
|
1623
|
+
block_name,
|
1624
|
+
opts: opts
|
1625
|
+
)[:code]).join("\n")
|
1626
|
+
|
1627
|
+
Dir::Tmpname.create(self.class.to_s) do |path|
|
1628
|
+
pp path
|
1629
|
+
File.write(path, code_blocks)
|
1630
|
+
ENV['MDE_LINK_REQUIRED_FILE'] = path
|
1631
|
+
end
|
1367
1632
|
end
|
1368
1633
|
end # class MarkParse
|
1369
1634
|
end # module MarkdownExec
|
@@ -1388,7 +1653,8 @@ if $PROGRAM_NAME == __FILE__
|
|
1388
1653
|
c.expects(:command_execute).with(
|
1389
1654
|
obj,
|
1390
1655
|
'',
|
1391
|
-
args: pigeon
|
1656
|
+
args: pigeon
|
1657
|
+
)
|
1392
1658
|
|
1393
1659
|
# Call method execute_approved_block
|
1394
1660
|
c.execute_approved_block(obj, [])
|
@@ -1415,7 +1681,8 @@ if $PROGRAM_NAME == __FILE__
|
|
1415
1681
|
}
|
1416
1682
|
]
|
1417
1683
|
|
1418
|
-
# iterate over the input and output data and
|
1684
|
+
# iterate over the input and output data and
|
1685
|
+
# assert that the method sets the title as expected
|
1419
1686
|
input_output_data.each do |data|
|
1420
1687
|
input = data[:input]
|
1421
1688
|
output = data[:output]
|
@@ -1424,13 +1691,5 @@ if $PROGRAM_NAME == __FILE__
|
|
1424
1691
|
end
|
1425
1692
|
end
|
1426
1693
|
end
|
1427
|
-
|
1428
|
-
###
|
1429
|
-
|
1430
|
-
# result = split_string_on_first_char("hello-world", "-")
|
1431
|
-
# puts result.inspect # Output should be ["hello", "world"]
|
1432
|
-
|
1433
|
-
# result = split_string_on_first_char("hello", "-")
|
1434
|
-
# puts result.inspect # Output should be ["hello", ""]
|
1435
1694
|
end
|
1436
1695
|
end
|