markdown_exec 1.3.8 → 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/CHANGELOG.md +41 -2
- data/Gemfile +1 -0
- data/Gemfile.lock +5 -1
- data/Rakefile +11 -7
- data/bin/bmde +11 -0
- data/bin/colorize_env_vars.sh +7 -0
- data/bin/tab_completion.sh +19 -19
- data/examples/duplicate_block.md +10 -0
- data/examples/import0.md +8 -0
- data/examples/import1.md +10 -0
- data/examples/include.md +12 -0
- data/examples/infile_config.md +10 -0
- data/examples/linked1.md +28 -0
- data/examples/linked2.md +29 -0
- data/examples/linked3.md +12 -0
- data/examples/opts.md +13 -0
- data/examples/pass-through.md +14 -0
- data/examples/plant.md +23 -0
- data/examples/port.md +23 -0
- data/examples/vars.md +20 -0
- data/examples/wrap.md +33 -0
- data/lib/block_types.rb +7 -0
- data/lib/cached_nested_file_reader.rb +0 -1
- data/lib/colorize.rb +61 -50
- data/lib/fcb.rb +12 -30
- data/lib/filter.rb +14 -10
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/markdown_exec.rb +1039 -541
- data/lib/mdoc.rb +106 -84
- data/lib/menu.src.yml +341 -267
- data/lib/menu.yml +342 -268
- data/lib/method_sorter.rb +76 -0
- data/lib/sort_yaml_gpt4.rb +32 -0
- metadata +22 -6
- data/lib/env_opts.rb +0 -242
- data/lib/markdown_block_manager.rb +0 -64
- data/lib/menu_options.rb +0 -0
- data/lib/menu_options.yml +0 -0
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,7 +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
|
+
|
40
|
+
# macros
|
41
|
+
#
|
42
|
+
LOAD_FILE = true
|
39
43
|
|
40
44
|
# custom error: file specified is missing
|
41
45
|
#
|
@@ -58,6 +62,18 @@ class Hash
|
|
58
62
|
end
|
59
63
|
end
|
60
64
|
|
65
|
+
# integer value for comparison
|
66
|
+
#
|
67
|
+
def options_fetch_display_level(options)
|
68
|
+
options.fetch(:display_level, 1)
|
69
|
+
end
|
70
|
+
|
71
|
+
# integer value for comparison
|
72
|
+
#
|
73
|
+
def options_fetch_display_level_xbase_prefix(options)
|
74
|
+
options.fetch(:level_xbase_prefix, '')
|
75
|
+
end
|
76
|
+
|
61
77
|
# stdout manager
|
62
78
|
#
|
63
79
|
module FOUT
|
@@ -77,7 +93,7 @@ module FOUT
|
|
77
93
|
end
|
78
94
|
|
79
95
|
def approved_fout?(level)
|
80
|
-
level <= @options
|
96
|
+
level <= options_fetch_display_level(@options)
|
81
97
|
end
|
82
98
|
|
83
99
|
# display output at level or lower than filter (DISPLAY_LEVEL_DEFAULT)
|
@@ -85,7 +101,7 @@ module FOUT
|
|
85
101
|
def lout(str, level: DISPLAY_LEVEL_BASE)
|
86
102
|
return unless approved_fout? level
|
87
103
|
|
88
|
-
fout level == DISPLAY_LEVEL_BASE ? str : @options
|
104
|
+
fout level == DISPLAY_LEVEL_BASE ? str : options_fetch_display_level_xbase_prefix(@options) + str
|
89
105
|
end
|
90
106
|
end
|
91
107
|
|
@@ -110,10 +126,45 @@ end
|
|
110
126
|
# convert regex match groups to a hash with symbol keys
|
111
127
|
#
|
112
128
|
# :reek:UtilityFunction
|
113
|
-
def
|
129
|
+
def extract_named_captures_from_option(str, option)
|
114
130
|
str.match(Regexp.new(option))&.named_captures&.sym_keys
|
115
131
|
end
|
116
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
|
+
|
117
168
|
# execute markdown documents
|
118
169
|
#
|
119
170
|
module MarkdownExec
|
@@ -121,6 +172,13 @@ module MarkdownExec
|
|
121
172
|
FNR11 = '/'
|
122
173
|
FNR12 = ',~'
|
123
174
|
|
175
|
+
SHELL_COLOR_OPTIONS = {
|
176
|
+
BLOCK_TYPE_BASH => :menu_bash_color,
|
177
|
+
BLOCK_TYPE_LINK => :menu_link_color,
|
178
|
+
BLOCK_TYPE_OPTS => :menu_opts_color,
|
179
|
+
BLOCK_TYPE_VARS => :menu_vars_color
|
180
|
+
}.freeze
|
181
|
+
|
124
182
|
##
|
125
183
|
#
|
126
184
|
# rubocop:disable Layout/LineLength
|
@@ -132,12 +190,11 @@ module MarkdownExec
|
|
132
190
|
class MarkParse
|
133
191
|
attr_reader :options
|
134
192
|
|
193
|
+
include ArrayUtil
|
194
|
+
include StringUtil
|
135
195
|
include FOUT
|
136
196
|
|
137
197
|
def initialize(options = {})
|
138
|
-
@options = options
|
139
|
-
# hide disabled symbol
|
140
|
-
@prompt = TTY::Prompt.new(interrupt: :exit, symbols: { cross: ' ' })
|
141
198
|
@execute_aborted_at = nil
|
142
199
|
@execute_completed_at = nil
|
143
200
|
@execute_error = nil
|
@@ -147,6 +204,74 @@ module MarkdownExec
|
|
147
204
|
@execute_script_filespec = nil
|
148
205
|
@execute_started_at = nil
|
149
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
|
150
275
|
end
|
151
276
|
|
152
277
|
# return arguments before `--`
|
@@ -162,17 +287,6 @@ module MarkdownExec
|
|
162
287
|
end
|
163
288
|
end
|
164
289
|
|
165
|
-
# return arguments after `--`
|
166
|
-
#
|
167
|
-
def arguments_for_child(argv = ARGV)
|
168
|
-
case ind = argv.find_index('--')
|
169
|
-
when nil, argv.count - 1
|
170
|
-
[]
|
171
|
-
else
|
172
|
-
argv[ind + 1..-1]
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
290
|
##
|
177
291
|
# options necessary to start, parse input, defaults for cli options
|
178
292
|
#
|
@@ -191,6 +305,14 @@ module MarkdownExec
|
|
191
305
|
end.compact.to_h
|
192
306
|
end
|
193
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
|
+
|
194
316
|
def calculated_options
|
195
317
|
{
|
196
318
|
bash: true, # bash block parsing in get_block_summary()
|
@@ -199,133 +321,84 @@ module MarkdownExec
|
|
199
321
|
}
|
200
322
|
end
|
201
323
|
|
202
|
-
#
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
#
|
208
|
-
# @param opts [Hash] Options hash containing configuration settings.
|
209
|
-
# @param mdoc [YourMDocClass] An instance of the MDoc class.
|
210
|
-
# @return [String] The name of the executed code block.
|
211
|
-
def approve_and_execute_block(opts, mdoc)
|
212
|
-
# Collect required code blocks based on the provided options.
|
213
|
-
required_blocks = mdoc.collect_recursively_required_code(opts[:block_name])
|
214
|
-
# Display required code blocks if requested or required approval.
|
215
|
-
if opts[:output_script] || opts[:user_must_approve]
|
216
|
-
display_required_code(opts,
|
217
|
-
required_blocks)
|
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
|
218
329
|
end
|
219
330
|
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
loop do
|
224
|
-
# Present a selection menu for user approval.
|
225
|
-
sel = @prompt.select(opts[:prompt_approve_block],
|
226
|
-
filter: true) do |menu|
|
227
|
-
menu.default 1
|
228
|
-
menu.choice opts[:prompt_yes], 1
|
229
|
-
menu.choice opts[:prompt_no], 2
|
230
|
-
menu.choice opts[:prompt_script_to_clipboard], 3
|
231
|
-
menu.choice opts[:prompt_save_script], 4
|
232
|
-
end
|
233
|
-
allow = (sel == 1)
|
234
|
-
if sel == 3
|
235
|
-
# Copy the code to the clipboard.
|
236
|
-
text = required_blocks.flatten.join($INPUT_RECORD_SEPARATOR)
|
237
|
-
Clipboard.copy(text)
|
238
|
-
fout "Clipboard updated: #{required_blocks.count} blocks," /
|
239
|
-
" #{required_blocks.flatten.count} lines," /
|
240
|
-
" #{text.length} characters"
|
241
|
-
end
|
242
|
-
if sel == 4
|
243
|
-
# Save the code to a file.
|
244
|
-
write_command_file(opts.merge(save_executed_script: true),
|
245
|
-
required_blocks)
|
246
|
-
fout "File saved: #{@options[:saved_filespec]}"
|
247
|
-
end
|
248
|
-
break if [1, 2].include? sel
|
249
|
-
end
|
331
|
+
unless File.exist? filename
|
332
|
+
fout 'Document is missing.'
|
333
|
+
return false
|
250
334
|
end
|
335
|
+
true
|
336
|
+
end
|
251
337
|
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
selected = mdoc.get_block_by_name(opts[:block_name])
|
338
|
+
def clear_required_file
|
339
|
+
ENV['MDE_LINK_REQUIRED_FILE'] = ''
|
340
|
+
end
|
256
341
|
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
342
|
+
# Collect required code blocks based on the provided options.
|
343
|
+
#
|
344
|
+
# @param opts [Hash] Options hash containing configuration settings.
|
345
|
+
# @param mdoc [YourMDocClass] An instance of the MDoc class.
|
346
|
+
# @return [Array<String>] Required code blocks as an array of lines.
|
347
|
+
def collect_required_code_lines(mdoc, selected, opts: {})
|
348
|
+
# Apply hash in opts block to environment variables
|
349
|
+
if selected[:shell] == BLOCK_TYPE_VARS
|
350
|
+
data = YAML.load(selected[:body].join("\n"))
|
351
|
+
data.each_key do |key|
|
352
|
+
ENV[key] = value = data[key].to_s
|
353
|
+
next unless opts[:menu_vars_set_format].present?
|
354
|
+
|
355
|
+
print format(
|
356
|
+
opts[:menu_vars_set_format],
|
357
|
+
{ key: key,
|
358
|
+
value: value }
|
359
|
+
).send(opts[:menu_vars_set_color].to_sym)
|
360
|
+
end
|
264
361
|
end
|
265
362
|
|
266
|
-
|
363
|
+
required = mdoc.collect_recursively_required_code(opts[:block_name], opts: opts)
|
364
|
+
read_required_blocks_from_temp_file + required[:code]
|
267
365
|
end
|
268
366
|
|
269
367
|
def cfile
|
270
|
-
@cfile ||= CachedNestedFileReader.new(
|
368
|
+
@cfile ||= CachedNestedFileReader.new(
|
369
|
+
import_pattern: @options.fetch(:import_pattern)
|
370
|
+
)
|
271
371
|
end
|
272
372
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
373
|
+
EF_STDOUT = :stdout
|
374
|
+
EF_STDERR = :stderr
|
375
|
+
EF_STDIN = :stdin
|
376
|
+
|
377
|
+
# Existing command_execute method
|
378
|
+
def command_execute(opts, command, args: [])
|
278
379
|
@execute_files = Hash.new([])
|
279
380
|
@execute_options = opts
|
280
381
|
@execute_started_at = Time.now.utc
|
281
382
|
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
Thread.new do
|
287
|
-
until (line = stdout.gets).nil?
|
288
|
-
@execute_files[EF_STDOUT] = @execute_files[EF_STDOUT] + [line]
|
289
|
-
print line if opts[:output_stdout]
|
290
|
-
yield nil, line, nil, exec_thr if block_given?
|
291
|
-
end
|
292
|
-
rescue IOError
|
293
|
-
#d 'stdout IOError, thread killed, do nothing'
|
383
|
+
Open3.popen3(opts[:shell], '-c', command, opts[:filename],
|
384
|
+
*args) do |stdin, stdout, stderr, exec_thr|
|
385
|
+
handle_stream(opts, stdout, EF_STDOUT) do |line|
|
386
|
+
yield nil, line, nil, exec_thr if block_given?
|
294
387
|
end
|
295
|
-
|
296
|
-
|
297
|
-
until (line = stderr.gets).nil?
|
298
|
-
@execute_files[EF_STDERR] = @execute_files[EF_STDERR] + [line]
|
299
|
-
print line if opts[:output_stdout]
|
300
|
-
yield nil, nil, line, exec_thr if block_given?
|
301
|
-
end
|
302
|
-
rescue IOError
|
303
|
-
#d 'stderr IOError, thread killed, do nothing'
|
388
|
+
handle_stream(opts, stderr, EF_STDERR) do |line|
|
389
|
+
yield nil, nil, line, exec_thr if block_given?
|
304
390
|
end
|
305
391
|
|
306
|
-
in_thr =
|
307
|
-
|
308
|
-
|
309
|
-
@execute_files[EF_STDIN] = @execute_files[EF_STDIN] + [line]
|
310
|
-
yield line, nil, nil, exec_thr if block_given?
|
311
|
-
end
|
312
|
-
#d 'exec_thr now dead'
|
313
|
-
rescue StandardError
|
314
|
-
#d 'stdin error, thread killed, do nothing'
|
392
|
+
in_thr = handle_stream(opts, $stdin, EF_STDIN) do |line|
|
393
|
+
stdin.puts(line)
|
394
|
+
yield line, nil, nil, exec_thr if block_given?
|
315
395
|
end
|
316
396
|
|
317
|
-
#d 'join exec_thr'
|
318
397
|
exec_thr.join
|
319
|
-
|
320
|
-
#d 'wait before closing stdin'
|
321
398
|
sleep 0.1
|
322
|
-
|
323
|
-
#d 'kill stdin thread'
|
324
|
-
in_thr.kill
|
325
|
-
# @return_code = exec_thr.value
|
326
|
-
#d 'command end'
|
399
|
+
in_thr.kill if in_thr&.alive?
|
327
400
|
end
|
328
|
-
|
401
|
+
|
329
402
|
@execute_completed_at = Time.now.utc
|
330
403
|
rescue Errno::ENOENT => err
|
331
404
|
#d 'command error ENOENT triggered by missing command in script'
|
@@ -343,31 +416,104 @@ module MarkdownExec
|
|
343
416
|
fout "Error ENOENT: #{err.inspect}"
|
344
417
|
end
|
345
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
|
+
|
346
427
|
def count_blocks_in_filename
|
347
|
-
|
428
|
+
fenced_start_and_end_regex = Regexp.new @options[:fenced_start_and_end_regex]
|
348
429
|
cnt = 0
|
349
430
|
cfile.readlines(@options[:filename]).each do |line|
|
350
|
-
cnt += 1 if line.match(
|
431
|
+
cnt += 1 if line.match(fenced_start_and_end_regex)
|
351
432
|
end
|
352
433
|
cnt / 2
|
353
434
|
end
|
354
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
|
+
|
355
475
|
# :reek:DuplicateMethodCall
|
356
|
-
def display_required_code(opts,
|
476
|
+
def display_required_code(opts, required_lines)
|
357
477
|
frame = opts[:output_divider].send(opts[:output_divider_color].to_sym)
|
358
478
|
fout frame
|
359
|
-
|
479
|
+
required_lines.each { |cb| fout cb }
|
360
480
|
fout frame
|
361
481
|
end
|
362
482
|
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
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
|
367
494
|
|
368
|
-
|
369
|
-
|
370
|
-
|
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)
|
371
517
|
|
372
518
|
simple_commands = {
|
373
519
|
doc_glob: -> { fout options[:md_filename_glob] },
|
@@ -398,27 +544,59 @@ module MarkdownExec
|
|
398
544
|
tab_completions: -> { fout tab_completions },
|
399
545
|
menu_export: -> { fout menu_export }
|
400
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)
|
401
563
|
simple_commands.each_key do |key|
|
402
564
|
if @options[key]
|
403
565
|
simple_commands[key].call
|
404
|
-
return
|
566
|
+
return true
|
405
567
|
end
|
406
568
|
end
|
569
|
+
false
|
570
|
+
end
|
407
571
|
|
408
|
-
|
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
|
409
577
|
#
|
410
|
-
|
411
|
-
|
412
|
-
bash: true,
|
413
|
-
struct: true
|
414
|
-
})
|
415
|
-
return unless @options[:output_saved_script_filename]
|
578
|
+
%i[blocks line]
|
579
|
+
end
|
416
580
|
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
581
|
+
## post-parse options configuration
|
582
|
+
#
|
583
|
+
def finalize_cli_argument_processing(rest)
|
584
|
+
## position 0: file or folder (optional)
|
585
|
+
#
|
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
|
595
|
+
|
596
|
+
## position 1: block name (optional)
|
597
|
+
#
|
598
|
+
block_name = rest.shift
|
599
|
+
@options[:block_name] = block_name if block_name.present?
|
422
600
|
end
|
423
601
|
|
424
602
|
## summarize blocks
|
@@ -426,7 +604,6 @@ module MarkdownExec
|
|
426
604
|
def get_block_summary(call_options, fcb)
|
427
605
|
opts = optsmerge call_options
|
428
606
|
# return fcb.body unless opts[:struct]
|
429
|
-
|
430
607
|
return fcb unless opts[:bash]
|
431
608
|
|
432
609
|
fcb.call = fcb.title.match(Regexp.new(opts[:block_calls_scan]))&.fetch(1, nil)
|
@@ -435,207 +612,282 @@ module MarkdownExec
|
|
435
612
|
else
|
436
613
|
fcb.title
|
437
614
|
end
|
438
|
-
bm =
|
439
|
-
fcb.stdin =
|
440
|
-
fcb.stdout =
|
441
|
-
|
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])
|
618
|
+
|
619
|
+
shell_color_option = SHELL_COLOR_OPTIONS[fcb[:shell]]
|
620
|
+
fcb.title = fcb.oname = bm && bm[1] ? bm[:title] : titlexcall
|
621
|
+
fcb.dname = if shell_color_option && opts[shell_color_option].present?
|
622
|
+
fcb.oname.send(opts[shell_color_option].to_sym)
|
623
|
+
else
|
624
|
+
fcb.oname
|
625
|
+
end
|
442
626
|
fcb
|
443
627
|
end
|
444
628
|
|
445
|
-
|
446
|
-
#
|
447
|
-
#
|
448
|
-
def
|
449
|
-
|
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
|
450
637
|
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
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
|
455
646
|
|
456
|
-
|
457
|
-
|
458
|
-
|
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)
|
459
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]
|
460
662
|
|
461
|
-
|
462
|
-
|
463
|
-
fcb = FCB.new
|
464
|
-
in_block = false
|
465
|
-
headings = []
|
663
|
+
[!LOAD_FILE, '']
|
664
|
+
end
|
466
665
|
|
467
|
-
|
468
|
-
|
469
|
-
|
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
|
676
|
+
|
677
|
+
history_state_push(mdoc, data_file, opts)
|
678
|
+
|
679
|
+
data.fetch('vars', []).each do |var|
|
680
|
+
ENV[var[0]] = var[1].to_s
|
681
|
+
end
|
470
682
|
|
471
|
-
|
472
|
-
|
683
|
+
[LOAD_FILE, data.fetch('block', '')]
|
684
|
+
end
|
473
685
|
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
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?
|
696
|
+
|
697
|
+
print format(
|
698
|
+
opts[:menu_opts_set_format],
|
699
|
+
{ key: key,
|
700
|
+
value: value }
|
701
|
+
).send(opts[:menu_opts_set_color].to_sym)
|
702
|
+
end
|
703
|
+
[!LOAD_FILE, '']
|
704
|
+
end
|
483
705
|
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
if block_given? &&
|
495
|
-
selected_messages.include?(:blocks) &&
|
496
|
-
Filter.fcb_select?(opts, fcb)
|
497
|
-
yield :blocks, fcb
|
498
|
-
end
|
499
|
-
end
|
500
|
-
in_block = false
|
501
|
-
else
|
502
|
-
# start fcb
|
503
|
-
#
|
504
|
-
in_block = true
|
505
|
-
|
506
|
-
fcb_title_groups = line.match(fenced_start_ex).named_captures.sym_keys
|
507
|
-
fcb = FCB.new
|
508
|
-
fcb.headings = headings
|
509
|
-
fcb.name = fcb_title_groups.fetch(:name, '')
|
510
|
-
fcb.shell = fcb_title_groups.fetch(:shell, '')
|
511
|
-
fcb.title = fcb_title_groups.fetch(:name, '')
|
512
|
-
|
513
|
-
# selected fcb
|
514
|
-
#
|
515
|
-
fcb.body = []
|
516
|
-
|
517
|
-
rest = fcb_title_groups.fetch(:rest, '')
|
518
|
-
fcb.reqs, fcb.wraps =
|
519
|
-
split_array(rest.scan(/\+[^\s]+/).map { |req| req[1..-1] }) do |name|
|
520
|
-
!name.match(Regexp.new(opts[:block_name_wrapper_match]))
|
521
|
-
end
|
522
|
-
fcb.call = rest.match(Regexp.new(opts[:block_calls_scan]))&.to_a&.first
|
523
|
-
fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
|
524
|
-
tn.named_captures.sym_keys
|
525
|
-
end
|
526
|
-
fcb.stdout = if (tn = rest.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/))
|
527
|
-
tn.named_captures.sym_keys
|
528
|
-
end
|
529
|
-
end
|
530
|
-
elsif in_block && fcb.body
|
531
|
-
dp 'append line to fcb body'
|
532
|
-
fcb.body += [line.chomp]
|
533
|
-
elsif block_given? && selected_messages.include?(:line)
|
534
|
-
dp 'text outside of fcb'
|
535
|
-
fcb = FCB.new
|
536
|
-
fcb.body = [line]
|
537
|
-
yield :line, fcb
|
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?
|
538
716
|
end
|
717
|
+
rescue IOError
|
718
|
+
#d 'stdout IOError, thread killed, do nothing'
|
539
719
|
end
|
540
720
|
end
|
541
721
|
|
542
|
-
def
|
543
|
-
|
544
|
-
|
722
|
+
def history_state_exist?
|
723
|
+
history = ENV.fetch(MDE_HISTORY_ENV_NAME, '')
|
724
|
+
history.present? ? history : nil
|
725
|
+
end
|
545
726
|
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
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
|
733
|
+
end
|
734
|
+
|
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
|
741
|
+
|
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
|
750
|
+
end
|
751
|
+
|
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")
|
757
|
+
|
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
|
551
768
|
end
|
552
769
|
end
|
770
|
+
@option_parser.load
|
771
|
+
@option_parser.environment
|
772
|
+
|
773
|
+
rest = @option_parser.parse!(arguments_for_mde)
|
774
|
+
@options[:pass_args] = ARGV[rest.count + 1..]
|
775
|
+
|
776
|
+
rest
|
777
|
+
end
|
778
|
+
|
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
|
553
795
|
|
554
|
-
|
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
|
+
}
|
555
805
|
end
|
556
806
|
|
557
|
-
#
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
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
|
562
818
|
|
563
|
-
|
564
|
-
|
819
|
+
update_line_and_block_state(line, state, opts, selected_messages, &block)
|
820
|
+
end
|
821
|
+
end
|
822
|
+
|
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.
|
565
830
|
#
|
566
831
|
def list_blocks_in_file(call_options = {}, &options_block)
|
567
832
|
opts = optsmerge(call_options, options_block)
|
568
833
|
use_chrome = !opts[:no_chrome]
|
569
834
|
|
570
835
|
blocks = []
|
571
|
-
|
572
|
-
blocks.push FCB.new({
|
573
|
-
# name: '',
|
574
|
-
chrome: true,
|
575
|
-
name: format(
|
576
|
-
opts[:menu_divider_format],
|
577
|
-
opts[:menu_initial_divider]
|
578
|
-
).send(opts[:menu_divider_color].to_sym),
|
579
|
-
disabled: '' # __LINE__.to_s
|
580
|
-
})
|
581
|
-
end
|
836
|
+
append_initial_divider(blocks, opts) if use_chrome
|
582
837
|
|
583
838
|
iter_blocks_in_file(opts) do |btype, fcb|
|
584
839
|
case btype
|
585
840
|
when :filter
|
586
|
-
|
587
|
-
#
|
588
|
-
%i[blocks line]
|
589
|
-
|
841
|
+
filter_block_types
|
590
842
|
when :line
|
591
|
-
|
592
|
-
#
|
593
|
-
if opts[:menu_divider_match].present? &&
|
594
|
-
(mbody = fcb.body[0].match opts[:menu_divider_match])
|
595
|
-
if use_chrome
|
596
|
-
blocks.push FCB.new(
|
597
|
-
{ chrome: true,
|
598
|
-
disabled: '',
|
599
|
-
name: format(opts[:menu_divider_format],
|
600
|
-
mbody[:name]).send(opts[:menu_divider_color].to_sym) }
|
601
|
-
)
|
602
|
-
end
|
603
|
-
elsif opts[:menu_task_match].present? &&
|
604
|
-
(fcb.body[0].match opts[:menu_task_match])
|
605
|
-
if use_chrome
|
606
|
-
blocks.push FCB.new(
|
607
|
-
{ chrome: true,
|
608
|
-
disabled: '',
|
609
|
-
name: format(
|
610
|
-
opts[:menu_task_format],
|
611
|
-
$~.named_captures.transform_keys(&:to_sym)
|
612
|
-
).send(opts[:menu_task_color].to_sym) }
|
613
|
-
)
|
614
|
-
end
|
615
|
-
else
|
616
|
-
# line not added
|
617
|
-
end
|
843
|
+
process_line_blocks(blocks, fcb, opts, use_chrome)
|
618
844
|
when :blocks
|
619
|
-
|
620
|
-
#
|
621
|
-
blocks.push get_block_summary(opts, fcb) ### if Filter.fcb_select? opts, fcb
|
845
|
+
append_block_summary(blocks, fcb, opts)
|
622
846
|
end
|
623
847
|
end
|
624
848
|
|
625
|
-
|
626
|
-
blocks.push FCB.new(
|
627
|
-
{ chrome: true,
|
628
|
-
disabled: '',
|
629
|
-
name: format(opts[:menu_divider_format],
|
630
|
-
opts[:menu_final_divider])
|
631
|
-
.send(opts[:menu_divider_color].to_sym) }
|
632
|
-
)
|
633
|
-
end
|
849
|
+
append_final_divider(blocks, opts) if use_chrome
|
634
850
|
blocks
|
635
851
|
rescue StandardError => err
|
636
|
-
|
637
|
-
|
638
|
-
|
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
|
639
891
|
end
|
640
892
|
|
641
893
|
def list_default_env
|
@@ -662,39 +914,22 @@ module MarkdownExec
|
|
662
914
|
|
663
915
|
def list_files_per_options(options)
|
664
916
|
list_files_specified(
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
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
|
+
)
|
669
923
|
)
|
670
924
|
end
|
671
925
|
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
[specified_folder, specified_filename]
|
680
|
-
else
|
681
|
-
[default_folder, specified_filename]
|
682
|
-
end
|
683
|
-
elsif specified_folder&.present?
|
684
|
-
if filetree
|
685
|
-
[specified_folder, @options[:md_filename_match]]
|
686
|
-
else
|
687
|
-
[specified_folder, @options[:md_filename_glob]]
|
688
|
-
end
|
689
|
-
else
|
690
|
-
[default_folder, default_filename]
|
691
|
-
end)
|
692
|
-
if filetree
|
693
|
-
filetree.select do |filename|
|
694
|
-
filename == fn || filename.match(/^#{fn}$/) || filename.match(%r{^#{fn}/.+$})
|
695
|
-
end
|
696
|
-
else
|
697
|
-
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}/.+$})
|
698
933
|
end
|
699
934
|
end
|
700
935
|
|
@@ -703,17 +938,6 @@ module MarkdownExec
|
|
703
938
|
@options[:md_filename_glob]))
|
704
939
|
end
|
705
940
|
|
706
|
-
def blocks_per_opts(blocks, opts)
|
707
|
-
if opts[:struct]
|
708
|
-
blocks
|
709
|
-
else
|
710
|
-
# blocks.map(&:name)
|
711
|
-
blocks.map do |block|
|
712
|
-
block.fetch(:text, nil) || block.fetch(:name, nil)
|
713
|
-
end
|
714
|
-
end.compact.reject(&:empty?)
|
715
|
-
end
|
716
|
-
|
717
941
|
## output type (body string or full object) per option struct and bash
|
718
942
|
#
|
719
943
|
def list_named_blocks_in_file(call_options = {}, &options_block)
|
@@ -726,6 +950,17 @@ module MarkdownExec
|
|
726
950
|
blocks_per_opts(blocks, opts)
|
727
951
|
end
|
728
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
|
+
|
729
964
|
def make_block_labels(call_options = {})
|
730
965
|
opts = options.merge(call_options)
|
731
966
|
list_blocks_in_file(opts).map do |fcb|
|
@@ -741,6 +976,32 @@ module MarkdownExec
|
|
741
976
|
end.compact
|
742
977
|
end
|
743
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
|
+
|
744
1005
|
# :reek:DuplicateMethodCall
|
745
1006
|
# :reek:NestedIterators
|
746
1007
|
def menu_for_optparse
|
@@ -764,11 +1025,11 @@ module MarkdownExec
|
|
764
1025
|
}
|
765
1026
|
when 'path'
|
766
1027
|
lambda { |value|
|
767
|
-
read_configuration_file!
|
1028
|
+
read_configuration_file!(options, value)
|
768
1029
|
}
|
769
1030
|
when 'show_config'
|
770
1031
|
lambda { |_|
|
771
|
-
|
1032
|
+
finalize_cli_argument_processing(options)
|
772
1033
|
fout options.sort_by_key.to_yaml
|
773
1034
|
}
|
774
1035
|
when 'val_as_bool'
|
@@ -792,33 +1053,14 @@ module MarkdownExec
|
|
792
1053
|
end
|
793
1054
|
end
|
794
1055
|
|
795
|
-
def
|
796
|
-
|
797
|
-
menu = []
|
798
|
-
iter_blocks_in_file(options) do |btype, fcb|
|
799
|
-
case btype
|
800
|
-
when :filter
|
801
|
-
%i[blocks line]
|
802
|
-
when :line
|
803
|
-
if options[:menu_divider_match] &&
|
804
|
-
(mbody = fcb.body[0].match(options[:menu_divider_match]))
|
805
|
-
menu.push FCB.new({ name: mbody[:name], disabled: '' })
|
806
|
-
end
|
807
|
-
when :blocks
|
808
|
-
menu += [fcb.name]
|
809
|
-
end
|
810
|
-
end
|
811
|
-
menu
|
1056
|
+
def menu_help
|
1057
|
+
@option_parser.help
|
812
1058
|
end
|
813
1059
|
|
814
1060
|
def menu_iter(data = menu_for_optparse, &block)
|
815
1061
|
data.map(&block)
|
816
1062
|
end
|
817
1063
|
|
818
|
-
def menu_help
|
819
|
-
@option_parser.help
|
820
|
-
end
|
821
|
-
|
822
1064
|
def menu_option_append(opts, options, item)
|
823
1065
|
return unless item[:long_name].present? || item[:short_name].present?
|
824
1066
|
|
@@ -846,27 +1088,6 @@ module MarkdownExec
|
|
846
1088
|
].compact)
|
847
1089
|
end
|
848
1090
|
|
849
|
-
## post-parse options configuration
|
850
|
-
#
|
851
|
-
def options_finalize(rest)
|
852
|
-
## position 0: file or folder (optional)
|
853
|
-
#
|
854
|
-
if (pos = rest.fetch(0, nil))&.present?
|
855
|
-
if Dir.exist?(pos)
|
856
|
-
@options[:path] = pos
|
857
|
-
elsif File.exist?(pos)
|
858
|
-
@options[:filename] = pos
|
859
|
-
else
|
860
|
-
raise FileMissingError, pos, caller
|
861
|
-
end
|
862
|
-
end
|
863
|
-
|
864
|
-
## position 1: block name (optional)
|
865
|
-
#
|
866
|
-
block_name = rest.fetch(1, nil)
|
867
|
-
@options[:block_name] = block_name if block_name.present?
|
868
|
-
end
|
869
|
-
|
870
1091
|
# :reek:ControlParameter
|
871
1092
|
def optsmerge(call_options = {}, options_block = nil)
|
872
1093
|
class_call_options = @options.merge(call_options || {})
|
@@ -910,25 +1131,113 @@ module MarkdownExec
|
|
910
1131
|
}
|
911
1132
|
end
|
912
1133
|
|
913
|
-
|
1134
|
+
# Prepare the blocks menu by adding labels and other necessary details.
|
914
1135
|
#
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
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
|
1158
|
+
|
1159
|
+
# Prepares and fetches file listings
|
1160
|
+
def prepare_file_list(options)
|
1161
|
+
list_files_per_options(options)
|
1162
|
+
end
|
1163
|
+
|
1164
|
+
def process_fenced_block(fcb, opts, selected_messages, &block)
|
1165
|
+
fcb.oname = fcb.dname = fcb.title || ''
|
1166
|
+
return unless fcb.body
|
1167
|
+
|
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
|
1175
|
+
end
|
1176
|
+
|
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.
|
1189
|
+
#
|
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
|
920
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
|
1218
|
+
end
|
1219
|
+
|
1220
|
+
## insert back option at head or tail
|
1221
|
+
#
|
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]
|
921
1231
|
end
|
922
1232
|
|
923
|
-
##
|
924
|
-
# insert exit option at head or tail
|
925
|
-
# return selected option or nil
|
1233
|
+
## insert exit option at head or tail
|
926
1234
|
#
|
927
|
-
def
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
1235
|
+
def prompt_menu_add_exit(items, label)
|
1236
|
+
if @options[:menu_exit_at_top]
|
1237
|
+
(@options[:menu_with_exit] ? [label] : []) + items
|
1238
|
+
else
|
1239
|
+
items + (@options[:menu_with_exit] ? [label] : [])
|
1240
|
+
end
|
932
1241
|
end
|
933
1242
|
|
934
1243
|
# :reek:UtilityFunction ### temp
|
@@ -939,57 +1248,33 @@ module MarkdownExec
|
|
939
1248
|
.transform_keys(&:to_sym))
|
940
1249
|
end
|
941
1250
|
|
942
|
-
#
|
943
|
-
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
|
948
|
-
## read local configuration file
|
949
|
-
#
|
950
|
-
read_configuration_file! @options,
|
951
|
-
".#{MarkdownExec::APP_NAME.downcase}.yml"
|
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 = []
|
952
1257
|
|
953
|
-
|
954
|
-
|
955
|
-
opts.banner = [
|
956
|
-
"#{MarkdownExec::APP_NAME}" \
|
957
|
-
" - #{MarkdownExec::APP_DESC} (#{MarkdownExec::VERSION})",
|
958
|
-
"Usage: #{executable_name} [(path | filename [block_name])] [options]"
|
959
|
-
].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?
|
960
1260
|
|
961
|
-
|
962
|
-
|
963
|
-
end
|
964
|
-
end
|
965
|
-
option_parser.load # filename defaults to basename of the program
|
966
|
-
# without suffix in a directory ~/.options
|
967
|
-
option_parser.environment # env defaults to the basename of the program
|
968
|
-
# child_argv = arguments_for_child
|
969
|
-
rest = option_parser.parse!(arguments_for_mde) # (into: options)
|
970
|
-
|
971
|
-
begin
|
972
|
-
options_finalize rest
|
973
|
-
exec_block options, options[:block_name]
|
974
|
-
rescue FileMissingError => err
|
975
|
-
puts "File missing: #{err}"
|
1261
|
+
if File.exist?(temp_blocks_file_path)
|
1262
|
+
temp_blocks = File.readlines(temp_blocks_file_path, chomp: true)
|
976
1263
|
end
|
1264
|
+
|
1265
|
+
temp_blocks
|
1266
|
+
end
|
1267
|
+
|
1268
|
+
def run
|
1269
|
+
clear_required_file
|
1270
|
+
execute_block_with_error_handling(initialize_and_parse_cli_options)
|
1271
|
+
delete_required_temp_file
|
977
1272
|
rescue StandardError => err
|
978
1273
|
warn(error = "ERROR ** MarkParse.run(); #{err.inspect}")
|
979
1274
|
binding.pry if $tap_enable
|
980
1275
|
raise ArgumentError, error
|
981
1276
|
end
|
982
1277
|
|
983
|
-
def saved_name_split(name)
|
984
|
-
# rubocop:disable Layout/LineLength
|
985
|
-
mf = /#{@options[:saved_script_filename_prefix]}_(?<time>[0-9\-]+)_(?<file>.+)_,_(?<block>.+)\.sh/.match name
|
986
|
-
# rubocop:enable Layout/LineLength
|
987
|
-
return unless mf
|
988
|
-
|
989
|
-
@options[:block_name] = mf[:block]
|
990
|
-
@options[:filename] = mf[:file].gsub(FNR12, FNR11)
|
991
|
-
end
|
992
|
-
|
993
1278
|
def run_last_script
|
994
1279
|
filename = SavedFilesMatcher.most_recent(@options[:saved_script_folder],
|
995
1280
|
@options[:saved_script_glob])
|
@@ -1000,30 +1285,20 @@ module MarkdownExec
|
|
1000
1285
|
select_approve_and_execute_block({})
|
1001
1286
|
end
|
1002
1287
|
|
1003
|
-
def
|
1004
|
-
|
1005
|
-
|
1006
|
-
@options[:
|
1007
|
-
|
1008
|
-
filename: File.basename(@options[:filename], '.*'),
|
1009
|
-
prefix: @options[:logged_stdout_filename_prefix],
|
1010
|
-
time: Time.now.utc)
|
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]}"
|
1292
|
+
end
|
1011
1293
|
|
1012
|
-
|
1013
|
-
|
1014
|
-
|
1015
|
-
|
1016
|
-
|
1017
|
-
FileUtils.mkdir_p dirname
|
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
|
1018
1299
|
|
1019
|
-
|
1020
|
-
|
1021
|
-
ol += ["\n-STDERR-\n"]
|
1022
|
-
ol += @execute_files&.fetch(EF_STDERR, [])
|
1023
|
-
ol += ["\n-STDIN-\n"]
|
1024
|
-
ol += @execute_files&.fetch(EF_STDIN, [])
|
1025
|
-
ol += ["\n"]
|
1026
|
-
File.write(@options[:logged_stdout_filespec], ol.join)
|
1300
|
+
@options[:block_name] = mf[:block]
|
1301
|
+
@options[:filename] = mf[:file].gsub(FNR12, FNR11)
|
1027
1302
|
end
|
1028
1303
|
|
1029
1304
|
# Select and execute a code block from a Markdown document.
|
@@ -1036,51 +1311,37 @@ module MarkdownExec
|
|
1036
1311
|
# @return [Nil] Returns nil if no code block is selected or an error occurs.
|
1037
1312
|
def select_approve_and_execute_block(call_options, &options_block)
|
1038
1313
|
opts = optsmerge(call_options, options_block)
|
1039
|
-
blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
|
1040
|
-
mdoc = MDoc.new(blocks_in_file) do |nopts|
|
1041
|
-
opts.merge!(nopts)
|
1042
|
-
end
|
1043
|
-
blocks_menu = mdoc.fcbs_per_options(opts.merge(struct: true))
|
1044
|
-
|
1045
1314
|
repeat_menu = true && !opts[:block_name].present?
|
1315
|
+
load_file = !LOAD_FILE
|
1316
|
+
default = 1
|
1317
|
+
|
1046
1318
|
loop do
|
1047
|
-
|
1048
|
-
|
1049
|
-
|
1050
|
-
|
1051
|
-
|
1052
|
-
|
1053
|
-
|
1054
|
-
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1066
|
-
|
1067
|
-
|
1068
|
-
return nil if bm.count.zero?
|
1069
|
-
|
1070
|
-
sel = prompt_with_quit(pt, bm, per_page: opts[:select_page_height])
|
1071
|
-
return nil if sel.nil?
|
1072
|
-
|
1073
|
-
## store selected option
|
1074
|
-
#
|
1075
|
-
label_block = blocks_in_file.select do |fcb|
|
1076
|
-
fcb[:label] == sel
|
1077
|
-
end.fetch(0, nil)
|
1078
|
-
opts[:block_name] = @options[:block_name] = label_block.fetch(:name, '')
|
1079
|
-
end
|
1080
|
-
approve_and_execute_block(opts, mdoc)
|
1081
|
-
break unless repeat_menu
|
1319
|
+
loop do
|
1320
|
+
opts[:back] = false
|
1321
|
+
blocks_in_file, blocks_menu, mdoc = load_file_and_prepare_menu(opts)
|
1322
|
+
|
1323
|
+
unless opts[:block_name].present?
|
1324
|
+
block_name, state = wait_for_user_selection(blocks_in_file, blocks_menu, default,
|
1325
|
+
opts)
|
1326
|
+
case state
|
1327
|
+
when :exit
|
1328
|
+
return nil
|
1329
|
+
when :back
|
1330
|
+
opts[:block_name] = block_name[:option]
|
1331
|
+
opts[:back] = true
|
1332
|
+
when :continue
|
1333
|
+
opts[:block_name] = block_name
|
1334
|
+
end
|
1335
|
+
end
|
1336
|
+
|
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
|
1082
1340
|
|
1083
|
-
|
1341
|
+
break if state == :continue && load_file == LOAD_FILE
|
1342
|
+
break unless repeat_menu
|
1343
|
+
end
|
1344
|
+
break if load_file != LOAD_FILE
|
1084
1345
|
end
|
1085
1346
|
rescue StandardError => err
|
1086
1347
|
warn(error = "ERROR ** MarkParse.select_approve_and_execute_block(); #{err.inspect}")
|
@@ -1089,25 +1350,53 @@ module MarkdownExec
|
|
1089
1350
|
raise ArgumentError, error
|
1090
1351
|
end
|
1091
1352
|
|
1092
|
-
def
|
1093
|
-
|
1094
|
-
|
1095
|
-
|
1096
|
-
|
1097
|
-
|
1098
|
-
|
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 }
|
1099
1388
|
end
|
1100
1389
|
end
|
1101
1390
|
|
1102
1391
|
def select_recent_output
|
1103
|
-
filename =
|
1392
|
+
filename = select_option_or_exit(
|
1104
1393
|
@options[:prompt_select_output].to_s,
|
1105
1394
|
list_recent_output(
|
1106
1395
|
@options[:saved_stdout_folder],
|
1107
1396
|
@options[:saved_stdout_glob],
|
1108
1397
|
@options[:list_count]
|
1109
1398
|
),
|
1110
|
-
{ per_page: @options[:select_page_height] }
|
1399
|
+
@options.merge({ per_page: @options[:select_page_height] })
|
1111
1400
|
)
|
1112
1401
|
return unless filename.present?
|
1113
1402
|
|
@@ -1115,14 +1404,14 @@ module MarkdownExec
|
|
1115
1404
|
end
|
1116
1405
|
|
1117
1406
|
def select_recent_script
|
1118
|
-
filename =
|
1407
|
+
filename = select_option_or_exit(
|
1119
1408
|
@options[:prompt_select_md].to_s,
|
1120
1409
|
list_recent_scripts(
|
1121
1410
|
@options[:saved_script_folder],
|
1122
1411
|
@options[:saved_script_glob],
|
1123
1412
|
@options[:list_count]
|
1124
1413
|
),
|
1125
|
-
{ per_page: @options[:select_page_height] }
|
1414
|
+
@options.merge({ per_page: @options[:select_page_height] })
|
1126
1415
|
)
|
1127
1416
|
return if filename.nil?
|
1128
1417
|
|
@@ -1135,11 +1424,37 @@ module MarkdownExec
|
|
1135
1424
|
})
|
1136
1425
|
end
|
1137
1426
|
|
1138
|
-
|
1139
|
-
|
1140
|
-
|
1141
|
-
|
1142
|
-
|
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]
|
1432
|
+
end
|
1433
|
+
|
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]))
|
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
|
1143
1458
|
end
|
1144
1459
|
|
1145
1460
|
def tab_completions(data = menu_for_optparse)
|
@@ -1148,6 +1463,77 @@ module MarkdownExec
|
|
1148
1463
|
end.compact
|
1149
1464
|
end
|
1150
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
|
+
|
1151
1537
|
# :reek:BooleanParameter
|
1152
1538
|
# :reek:ControlParameter
|
1153
1539
|
def update_options(opts = {}, over: true)
|
@@ -1159,7 +1545,30 @@ module MarkdownExec
|
|
1159
1545
|
@options
|
1160
1546
|
end
|
1161
1547
|
|
1162
|
-
|
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.
|
1571
|
+
def write_command_file(call_options, required_lines)
|
1163
1572
|
return unless call_options[:save_executed_script]
|
1164
1573
|
|
1165
1574
|
time_now = Time.now.utc
|
@@ -1174,24 +1583,113 @@ module MarkdownExec
|
|
1174
1583
|
@options[:saved_filespec] =
|
1175
1584
|
File.join opts[:saved_script_folder], opts[:saved_script_filename]
|
1176
1585
|
|
1177
|
-
dirname = File.dirname(@options[:saved_filespec])
|
1178
|
-
FileUtils.mkdir_p dirname
|
1179
1586
|
shebang = if @options[:shebang]&.present?
|
1180
1587
|
"#{@options[:shebang]} #{@options[:shell]}\n"
|
1181
1588
|
else
|
1182
1589
|
''
|
1183
1590
|
end
|
1184
1591
|
|
1185
|
-
|
1186
|
-
|
1187
|
-
|
1188
|
-
|
1189
|
-
|
1190
|
-
|
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"
|
1597
|
+
|
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
|
1191
1614
|
|
1192
|
-
|
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
|
1193
1632
|
end
|
1194
1633
|
end # class MarkParse
|
1195
1634
|
end # module MarkdownExec
|
1196
1635
|
|
1197
|
-
|
1636
|
+
if $PROGRAM_NAME == __FILE__
|
1637
|
+
require 'bundler/setup'
|
1638
|
+
Bundler.require(:default)
|
1639
|
+
|
1640
|
+
require 'minitest/autorun'
|
1641
|
+
|
1642
|
+
module MarkdownExec
|
1643
|
+
class TestMarkParse < Minitest::Test
|
1644
|
+
require 'mocha/minitest'
|
1645
|
+
|
1646
|
+
def test_calling_execute_approved_block_calls_command_execute_with_argument_args_value
|
1647
|
+
pigeon = 'E'
|
1648
|
+
obj = { pass_args: pigeon }
|
1649
|
+
|
1650
|
+
c = MarkdownExec::MarkParse.new
|
1651
|
+
|
1652
|
+
# Expect that method command_execute is called with argument args having value pigeon
|
1653
|
+
c.expects(:command_execute).with(
|
1654
|
+
obj,
|
1655
|
+
'',
|
1656
|
+
args: pigeon
|
1657
|
+
)
|
1658
|
+
|
1659
|
+
# Call method execute_approved_block
|
1660
|
+
c.execute_approved_block(obj, [])
|
1661
|
+
end
|
1662
|
+
|
1663
|
+
def setup
|
1664
|
+
@mark_parse = MarkdownExec::MarkParse.new
|
1665
|
+
end
|
1666
|
+
|
1667
|
+
def test_set_fcb_title
|
1668
|
+
# sample input and output data for testing set_fcb_title method
|
1669
|
+
input_output_data = [
|
1670
|
+
{
|
1671
|
+
input: FCB.new(title: nil, body: ["puts 'Hello, world!'"]),
|
1672
|
+
output: "puts 'Hello, world!'"
|
1673
|
+
},
|
1674
|
+
{
|
1675
|
+
input: FCB.new(title: '', body: ['def add(x, y)', ' x + y', 'end']),
|
1676
|
+
output: 'def add(x, y) x + y end'
|
1677
|
+
},
|
1678
|
+
{
|
1679
|
+
input: FCB.new(title: 'foo', body: %w[bar baz]),
|
1680
|
+
output: 'foo' # expect the title to remain unchanged
|
1681
|
+
}
|
1682
|
+
]
|
1683
|
+
|
1684
|
+
# iterate over the input and output data and
|
1685
|
+
# assert that the method sets the title as expected
|
1686
|
+
input_output_data.each do |data|
|
1687
|
+
input = data[:input]
|
1688
|
+
output = data[:output]
|
1689
|
+
@mark_parse.set_fcb_title(input)
|
1690
|
+
assert_equal output, input.title
|
1691
|
+
end
|
1692
|
+
end
|
1693
|
+
end
|
1694
|
+
end
|
1695
|
+
end
|