markdown_exec 1.3.7 → 1.3.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/Gemfile.lock +1 -1
- data/Rakefile +1 -0
- data/bin/tab_completion.sh +2 -2
- data/lib/filter.rb +140 -68
- data/lib/markdown_block_manager.rb +26 -157
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/markdown_exec.rb +69 -20
- data/lib/mdoc.rb +153 -41
- data/lib/menu.src.yml +12 -2
- data/lib/menu.yml +13 -3
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1f61a046f9f071edb4e123fd5ad1972de408c4a40c97d923322e416f643f4ffb
|
4
|
+
data.tar.gz: 8d00be273b75ac093809cbc168e585d36da49a26fa0bdde6ee242d06a94c3caf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 054de96356ebb625db55bccb693e498abebf01ab7eeb44b28f6391c3fec3ca87b72970916cbc3ba4dca9088905206b47a33213c9ded81fa67458c47827ba7068
|
7
|
+
data.tar.gz: 642a9e511a013c4485eb187e4be1d22a6516686b8815de1c6b1c230790269612357e9635d0b7b078dbc8f334b1e2d2e06980af71330f3bf8c1a273861bf902f4
|
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
data/Rakefile
CHANGED
data/bin/tab_completion.sh
CHANGED
@@ -13,7 +13,7 @@ __filedirs_all()
|
|
13
13
|
}
|
14
14
|
|
15
15
|
_mde_echo_version() {
|
16
|
-
echo "1.3.
|
16
|
+
echo "1.3.8"
|
17
17
|
}
|
18
18
|
|
19
19
|
_mde() {
|
@@ -138,4 +138,4 @@ _mde() {
|
|
138
138
|
|
139
139
|
complete -o filenames -o nospace -F _mde mde
|
140
140
|
# _mde_echo_version
|
141
|
-
# echo "Updated: 2023-10-
|
141
|
+
# echo "Updated: 2023-10-20 23:59:34 UTC"
|
data/lib/filter.rb
CHANGED
@@ -6,13 +6,22 @@
|
|
6
6
|
module MarkdownExec
|
7
7
|
# Filter
|
8
8
|
#
|
9
|
-
# The Filter class provides utilities to determine the inclusion of
|
10
|
-
# based on a set of provided options. The
|
11
|
-
# various properties of an
|
9
|
+
# The Filter class provides utilities to determine the inclusion of
|
10
|
+
# fenced code blocks (FCB) based on a set of provided options. The
|
11
|
+
# primary function, `fcb_select?`, checks various properties of an
|
12
|
+
# FCB and decides whether to include or exclude it.
|
12
13
|
#
|
13
14
|
# :reek:UtilityFunction
|
14
|
-
|
15
15
|
class Filter
|
16
|
+
# Determines whether to include or exclude a fenced code block
|
17
|
+
# (FCB) based on the provided options.
|
18
|
+
#
|
19
|
+
# @param options [Hash] The options used for filtering FCBs.
|
20
|
+
# @param fcb [Hash] The fenced code block to be evaluated.
|
21
|
+
# @return [Boolean] True if the FCB should be included; false if
|
22
|
+
# it should be excluded.
|
23
|
+
# @raise [StandardError] If an error occurs during the evaluation.
|
24
|
+
#
|
16
25
|
def self.fcb_select?(options, fcb)
|
17
26
|
filters = {
|
18
27
|
name_default: true,
|
@@ -21,7 +30,9 @@ module MarkdownExec
|
|
21
30
|
shell_default: true,
|
22
31
|
shell_exclude: nil,
|
23
32
|
shell_select: nil,
|
24
|
-
hidden_name: nil
|
33
|
+
hidden_name: nil,
|
34
|
+
include_name: nil,
|
35
|
+
wrap_name: nil
|
25
36
|
}
|
26
37
|
|
27
38
|
name = fcb.fetch(:name, '')
|
@@ -37,6 +48,14 @@ module MarkdownExec
|
|
37
48
|
raise err
|
38
49
|
end
|
39
50
|
|
51
|
+
# Applies name-based filters to determine whether to include or
|
52
|
+
# exclude a fenced code block (FCB)
|
53
|
+
# based on the block's name and provided options.
|
54
|
+
#
|
55
|
+
# @param options [Hash] The options used for filtering FCBs.
|
56
|
+
# @param filters [Hash] The filter settings to be updated.
|
57
|
+
# @param name [String] The name of the fenced code block.
|
58
|
+
#
|
40
59
|
def self.apply_name_filters(options, filters, name)
|
41
60
|
if name.present? && options[:block_name]
|
42
61
|
if name =~ /#{options[:block_name]}/
|
@@ -59,6 +78,14 @@ module MarkdownExec
|
|
59
78
|
filters[:name_exclude] = !!(name =~ /#{options[:exclude_by_name_regex]}/)
|
60
79
|
end
|
61
80
|
|
81
|
+
# Applies shell-based filters to determine whether to include or
|
82
|
+
# exclude a fenced code block (FCB)
|
83
|
+
# based on the block's shell type and provided options.
|
84
|
+
#
|
85
|
+
# @param options [Hash] The options used for filtering FCBs.
|
86
|
+
# @param filters [Hash] The filter settings to be updated.
|
87
|
+
# @param shell [String] The shell type of the fenced code block.
|
88
|
+
#
|
62
89
|
def self.apply_shell_filters(options, filters, shell)
|
63
90
|
filters[:shell_expect] = shell == 'expect'
|
64
91
|
|
@@ -71,34 +98,58 @@ module MarkdownExec
|
|
71
98
|
filters[:shell_exclude] = !!(shell =~ /#{options[:exclude_by_shell_regex]}/)
|
72
99
|
end
|
73
100
|
|
101
|
+
# Applies additional filters to determine whether to include or
|
102
|
+
# exclude a fenced code block (FCB)
|
103
|
+
# based on various criteria and provided options.
|
104
|
+
#
|
105
|
+
# @param options [Hash] The options used for filtering FCBs.
|
106
|
+
# @param filters [Hash] The filter settings to be updated.
|
107
|
+
# @param fcb [Hash] The fenced code block to be evaluated.
|
108
|
+
#
|
74
109
|
def self.apply_other_filters(options, filters, fcb)
|
75
110
|
name = fcb.fetch(:name, '')
|
76
111
|
shell = fcb.fetch(:shell, '')
|
77
112
|
filters[:fcb_chrome] = fcb.fetch(:chrome, false)
|
78
113
|
|
79
|
-
if name.present? && options[:hide_blocks_by_name]
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
if shell.present? && options[:hide_blocks_by_shell] &&
|
85
|
-
options[:block_shell_hidden_match].present?
|
86
|
-
!!(shell =~ /#{options[:block_shell_hidden_match]}/)
|
114
|
+
if name.present? && options[:hide_blocks_by_name]
|
115
|
+
filters[:hidden_name] =
|
116
|
+
!!(options[:block_name_hidden_match].present? &&
|
117
|
+
name =~ /#{options[:block_name_hidden_match]}/)
|
87
118
|
end
|
119
|
+
filters[:include_name] =
|
120
|
+
!!(options[:block_name_include_match].present? &&
|
121
|
+
name =~ /#{options[:block_name_include_match]}/)
|
122
|
+
filters[:wrap_name] =
|
123
|
+
!!(options[:block_name_wrapper_match].present? &&
|
124
|
+
name =~ /#{options[:block_name_wrapper_match]}/)
|
88
125
|
|
89
126
|
return unless options[:bash_only]
|
90
127
|
|
91
128
|
filters[:shell_default] = (shell == 'bash')
|
92
129
|
end
|
93
130
|
|
131
|
+
# Evaluates the filter settings to make a final decision on
|
132
|
+
# whether to include or exclude a fenced
|
133
|
+
# code block (FCB) based on the provided options.
|
134
|
+
#
|
135
|
+
# @param options [Hash] The options used for filtering FCBs.
|
136
|
+
# @param filters [Hash] The filter settings to be evaluated.
|
137
|
+
# @return [Boolean] True if the FCB should be included; false
|
138
|
+
# if it should be excluded.
|
139
|
+
#
|
94
140
|
def self.evaluate_filters(options, filters)
|
95
|
-
if options[:no_chrome] && filters[:fcb_chrome]
|
141
|
+
if options[:no_chrome] && filters[:fcb_chrome] == true
|
96
142
|
false
|
97
|
-
elsif options[:exclude_expect_blocks] && filters[:shell_expect]
|
143
|
+
elsif options[:exclude_expect_blocks] && filters[:shell_expect] == true
|
98
144
|
false
|
99
145
|
elsif filters[:hidden_name] == true
|
146
|
+
false
|
147
|
+
elsif filters[:include_name] == true
|
100
148
|
true
|
101
|
-
elsif filters[:
|
149
|
+
elsif filters[:wrap_name] == true
|
150
|
+
true
|
151
|
+
elsif filters[:name_exclude] == true || filters[:shell_exclude] == true ||
|
152
|
+
filters[:name_select] == false || filters[:shell_select] == false
|
102
153
|
false
|
103
154
|
elsif filters[:name_select] == true || filters[:shell_select] == true
|
104
155
|
true
|
@@ -112,70 +163,91 @@ module MarkdownExec
|
|
112
163
|
end
|
113
164
|
|
114
165
|
if $PROGRAM_NAME == __FILE__
|
166
|
+
require 'bundler/setup'
|
167
|
+
Bundler.require(:default)
|
168
|
+
|
115
169
|
require 'minitest/autorun'
|
116
170
|
|
117
|
-
|
118
|
-
|
171
|
+
module MarkdownExec
|
172
|
+
class FilterTest < Minitest::Test
|
173
|
+
def setup
|
174
|
+
@options = {}
|
175
|
+
@fcb = {}
|
176
|
+
end
|
119
177
|
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
178
|
+
# Tests for fcb_select? method
|
179
|
+
def test_no_chrome_condition
|
180
|
+
@options[:no_chrome] = true
|
181
|
+
@fcb[:chrome] = true
|
182
|
+
refute Filter.fcb_select?(@options, @fcb)
|
183
|
+
end
|
126
184
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
185
|
+
def test_exclude_expect_blocks_condition
|
186
|
+
@options[:exclude_expect_blocks] = true
|
187
|
+
@fcb[:shell] = 'expect'
|
188
|
+
refute Filter.fcb_select?(@options, @fcb)
|
189
|
+
end
|
132
190
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
191
|
+
def test_hidden_name_condition
|
192
|
+
@options[:hide_blocks_by_name] = true
|
193
|
+
@options[:block_name_hidden_match] = 'hidden'
|
194
|
+
@fcb[:name] = 'hidden_block'
|
195
|
+
refute Filter.fcb_select?(@options, @fcb)
|
196
|
+
end
|
138
197
|
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
198
|
+
def test_include_name_condition
|
199
|
+
@options[:hide_blocks_by_name] = true
|
200
|
+
@options[:block_name_indlude_match] = 'include'
|
201
|
+
@fcb[:name] = 'include_block'
|
202
|
+
assert Filter.fcb_select?(@options, @fcb)
|
203
|
+
end
|
144
204
|
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
205
|
+
def test_wrap_name_condition
|
206
|
+
@options[:hide_blocks_by_name] = true
|
207
|
+
@options[:block_name_wrapper_match] = 'wrap'
|
208
|
+
@fcb[:name] = 'wrap_block'
|
209
|
+
assert Filter.fcb_select?(@options, @fcb)
|
210
|
+
end
|
150
211
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
212
|
+
def test_name_exclude_condition
|
213
|
+
@options[:block_name] = 'test'
|
214
|
+
@fcb[:name] = 'sample'
|
215
|
+
refute Filter.fcb_select?(@options, @fcb)
|
216
|
+
end
|
156
217
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
218
|
+
def test_shell_exclude_condition
|
219
|
+
@options[:exclude_by_shell_regex] = 'exclude_this'
|
220
|
+
@fcb[:shell] = 'exclude_this_shell'
|
221
|
+
refute Filter.fcb_select?(@options, @fcb)
|
222
|
+
end
|
162
223
|
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
224
|
+
def test_name_select_condition
|
225
|
+
@options[:select_by_name_regex] = 'select'
|
226
|
+
@fcb[:name] = 'select_this'
|
227
|
+
assert Filter.fcb_select?(@options, @fcb)
|
228
|
+
end
|
168
229
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
230
|
+
def test_shell_select_condition
|
231
|
+
@options[:select_by_shell_regex] = 'select_this'
|
232
|
+
@fcb[:shell] = 'select_this_shell'
|
233
|
+
assert Filter.fcb_select?(@options, @fcb)
|
234
|
+
end
|
235
|
+
|
236
|
+
def test_bash_only_condition_true
|
237
|
+
@options[:bash_only] = true
|
238
|
+
@fcb[:shell] = 'bash'
|
239
|
+
assert Filter.fcb_select?(@options, @fcb)
|
240
|
+
end
|
174
241
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
242
|
+
def test_bash_only_condition_false
|
243
|
+
@options[:bash_only] = true
|
244
|
+
@fcb[:shell] = 'zsh'
|
245
|
+
refute Filter.fcb_select?(@options, @fcb)
|
246
|
+
end
|
247
|
+
|
248
|
+
def test_default_case
|
249
|
+
assert Filter.fcb_select?(@options, @fcb)
|
250
|
+
end
|
179
251
|
end
|
180
252
|
end
|
181
253
|
end
|
@@ -16,115 +16,25 @@ module MarkdownExec
|
|
16
16
|
class MarkdownBlockManager
|
17
17
|
attr_reader :block_table
|
18
18
|
|
19
|
+
# initialize
|
20
|
+
#
|
21
|
+
# Initializes an instance of MarkdownBlockManager with the given block table.
|
22
|
+
#
|
23
|
+
# @param block_table [Array<Hash>] An array of hashes representing markdown blocks.
|
19
24
|
def initialize(block_table)
|
20
25
|
@block_table = block_table
|
21
26
|
end
|
22
27
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
28
|
+
# get_block
|
29
|
+
#
|
30
|
+
# Retrieves a code block by its name.
|
31
|
+
#
|
32
|
+
# @param name [String] The name of the code block to retrieve.
|
33
|
+
# @param default [Hash] The default value to return if the code block is not found.
|
34
|
+
# @return [Hash] The code block as a hash or the default value if not found.
|
30
35
|
def get_block(name, default = {})
|
31
36
|
@block_table.select { |block| block.fetch(:name, '') == name }.fetch(0, default)
|
32
37
|
end
|
33
|
-
|
34
|
-
def gather_required_blocks(name)
|
35
|
-
named_block = get_block(name)
|
36
|
-
if named_block.nil? || named_block.keys.empty?
|
37
|
-
raise "Named code block `#{name}` not found."
|
38
|
-
end
|
39
|
-
|
40
|
-
all_blocks = [named_block.fetch(:name, '')] + required_blocks(named_block[:reqs])
|
41
|
-
@block_table.select { |block| all_blocks.include? block.fetch(:name, '') }
|
42
|
-
.map do |block|
|
43
|
-
process_block_references(block)
|
44
|
-
end.flatten(1)
|
45
|
-
end
|
46
|
-
|
47
|
-
def hide_block_given_options(opts, block)
|
48
|
-
(opts[:hide_blocks_by_name] &&
|
49
|
-
block[:name]&.match(Regexp.new(opts[:block_name_hidden_match])) &&
|
50
|
-
(block[:name]&.present? || block[:label]&.present?)
|
51
|
-
)
|
52
|
-
end
|
53
|
-
|
54
|
-
def blocks_per_options(opts = {})
|
55
|
-
filtered_blocks = @block_table.select do |block_group|
|
56
|
-
Filter.block_selected? opts, block_group
|
57
|
-
end
|
58
|
-
if opts[:hide_blocks_by_name]
|
59
|
-
filtered_blocks.reject { |block| hide_block_given_options opts, block }
|
60
|
-
else
|
61
|
-
filtered_blocks
|
62
|
-
end.map do |block|
|
63
|
-
block
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
def required_blocks(reqs)
|
68
|
-
return [] unless reqs
|
69
|
-
|
70
|
-
remaining = reqs
|
71
|
-
collected = []
|
72
|
-
while remaining.count.positive?
|
73
|
-
remaining = remaining.map do |req|
|
74
|
-
next if collected.include? req
|
75
|
-
|
76
|
-
collected += [req]
|
77
|
-
get_block(req).fetch(:reqs, [])
|
78
|
-
end
|
79
|
-
.compact
|
80
|
-
.flatten(1)
|
81
|
-
end
|
82
|
-
collected
|
83
|
-
end
|
84
|
-
|
85
|
-
# Helper method to process block code
|
86
|
-
def process_block_code(block)
|
87
|
-
body = block[:body].join("\n")
|
88
|
-
|
89
|
-
if block[:cann]
|
90
|
-
xcall = block[:cann][1..-2]
|
91
|
-
mstdin = xcall.match(/<(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/)
|
92
|
-
mstdout = xcall.match(/>(?<type>\$)?(?<name>[A-Za-z_\-.\w]+)/)
|
93
|
-
|
94
|
-
yqcmd = if mstdin[:type]
|
95
|
-
"echo \"$#{mstdin[:name]}\" | yq '#{body}'"
|
96
|
-
else
|
97
|
-
"yq e '#{body}' '#{mstdin[:name]}'"
|
98
|
-
end
|
99
|
-
if mstdout[:type]
|
100
|
-
"export #{mstdout[:name]}=$(#{yqcmd})"
|
101
|
-
else
|
102
|
-
"#{yqcmd} > '#{mstdout[:name]}'"
|
103
|
-
end
|
104
|
-
elsif block[:stdout]
|
105
|
-
stdout = block[:stdout]
|
106
|
-
body = block[:body].join("\n")
|
107
|
-
if stdout[:type]
|
108
|
-
%(export #{stdout[:name]}=$(cat <<"EOF"\n#{body}\nEOF\n))
|
109
|
-
else
|
110
|
-
"cat > '#{stdout[:name]}' <<\"EOF\"\n" \
|
111
|
-
"#{body}\n" \
|
112
|
-
"EOF\n"
|
113
|
-
end
|
114
|
-
else
|
115
|
-
block[:body]
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
# Helper method to process block references
|
120
|
-
def process_block_references(block)
|
121
|
-
if (call = block[:call])
|
122
|
-
[get_block("[#{call.match(/^%\((\S+) |\)/)[1]}]")
|
123
|
-
.merge({ cann: call })]
|
124
|
-
else
|
125
|
-
[]
|
126
|
-
end + [block]
|
127
|
-
end
|
128
38
|
end
|
129
39
|
end
|
130
40
|
|
@@ -134,62 +44,21 @@ if $PROGRAM_NAME == __FILE__
|
|
134
44
|
|
135
45
|
require 'minitest/autorun'
|
136
46
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
{ name: 'block3', body: ['code for block3'], reqs: ['block1'] }
|
147
|
-
]
|
148
|
-
@doc = MDoc.new(@table)
|
149
|
-
end
|
150
|
-
|
151
|
-
def test_collect_recursively_required_code
|
152
|
-
result = @doc.collect_recursively_required_code('block1')
|
153
|
-
expected_result = @table[0][:body] + @table[1][:body]
|
154
|
-
assert_equal expected_result, result
|
155
|
-
end
|
156
|
-
|
157
|
-
def test_get_block_by_name
|
158
|
-
result = @doc.get_block_by_name('block1')
|
159
|
-
assert_equal @table[0], result
|
160
|
-
|
161
|
-
result_missing = @doc.get_block_by_name('missing_block')
|
162
|
-
assert_equal({}, result_missing)
|
163
|
-
end
|
164
|
-
|
165
|
-
def test_get_required_blocks
|
166
|
-
result = @doc.get_required_blocks('block3')
|
167
|
-
expected_result = [@table[0], @table[1], @table[2]]
|
168
|
-
assert_equal expected_result, result
|
169
|
-
|
170
|
-
assert_raises(RuntimeError) { @doc.get_required_blocks('missing_block') }
|
171
|
-
end
|
172
|
-
|
173
|
-
def test_hide_menu_block_per_options
|
174
|
-
opts = { hide_blocks_by_name: true, block_name_hidden_match: 'block1' }
|
175
|
-
block = { name: 'block1' }
|
176
|
-
result = @doc.hide_menu_block_per_options(opts, block)
|
177
|
-
assert result # this should be true based on the given logic
|
178
|
-
end
|
179
|
-
|
180
|
-
def test_fcbs_per_options
|
181
|
-
opts = { hide_blocks_by_name: true, block_name_hidden_match: 'block1' }
|
182
|
-
result = @doc.fcbs_per_options(opts)
|
183
|
-
assert_equal [@table[1], @table[2]], result
|
184
|
-
end
|
185
|
-
|
186
|
-
def test_recursively_required
|
187
|
-
result = @doc.recursively_required(['block3'])
|
188
|
-
assert_equal %w[block3 block1 block2], result
|
47
|
+
class MarkdownBlockManagerTest < Minitest::Test
|
48
|
+
def setup
|
49
|
+
@block_table = [
|
50
|
+
{ name: 'block1', reqs: ['block2'], body: ['code1'] },
|
51
|
+
{ name: 'block2', body: ['code2'] },
|
52
|
+
{ name: 'block3', body: ['code3'] }
|
53
|
+
]
|
54
|
+
@manager = MarkdownExec::MarkdownBlockManager.new(@block_table)
|
55
|
+
end
|
189
56
|
|
190
|
-
|
191
|
-
|
192
|
-
|
57
|
+
# Test the get_block method
|
58
|
+
def test_get_block
|
59
|
+
block = @manager.get_block('block2')
|
60
|
+
expected_block = { name: 'block2', body: ['code2'] }
|
61
|
+
assert_equal expected_block, block
|
193
62
|
end
|
194
63
|
end
|
195
64
|
end
|
data/lib/markdown_exec.rb
CHANGED
@@ -199,26 +199,40 @@ module MarkdownExec
|
|
199
199
|
}
|
200
200
|
end
|
201
201
|
|
202
|
+
# Execute a code block after approval and provide user interaction options.
|
203
|
+
#
|
204
|
+
# This method displays required code blocks, asks for user approval, and
|
205
|
+
# executes the code block if approved. It also allows users to copy the
|
206
|
+
# code to the clipboard or save it to a file.
|
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.
|
202
211
|
def approve_and_execute_block(opts, mdoc)
|
212
|
+
# Collect required code blocks based on the provided options.
|
203
213
|
required_blocks = mdoc.collect_recursively_required_code(opts[:block_name])
|
214
|
+
# Display required code blocks if requested or required approval.
|
204
215
|
if opts[:output_script] || opts[:user_must_approve]
|
205
216
|
display_required_code(opts,
|
206
217
|
required_blocks)
|
207
218
|
end
|
208
219
|
|
209
220
|
allow = true
|
221
|
+
# If user approval is required, prompt the user for approval.
|
210
222
|
if opts[:user_must_approve]
|
211
223
|
loop do
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
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
|
220
233
|
allow = (sel == 1)
|
221
234
|
if sel == 3
|
235
|
+
# Copy the code to the clipboard.
|
222
236
|
text = required_blocks.flatten.join($INPUT_RECORD_SEPARATOR)
|
223
237
|
Clipboard.copy(text)
|
224
238
|
fout "Clipboard updated: #{required_blocks.count} blocks," /
|
@@ -226,6 +240,7 @@ module MarkdownExec
|
|
226
240
|
" #{text.length} characters"
|
227
241
|
end
|
228
242
|
if sel == 4
|
243
|
+
# Save the code to a file.
|
229
244
|
write_command_file(opts.merge(save_executed_script: true),
|
230
245
|
required_blocks)
|
231
246
|
fout "File saved: #{@options[:saved_filespec]}"
|
@@ -233,13 +248,16 @@ module MarkdownExec
|
|
233
248
|
break if [1, 2].include? sel
|
234
249
|
end
|
235
250
|
end
|
236
|
-
(opts[:ir_approve] = allow)
|
237
251
|
|
238
|
-
|
252
|
+
opts[:ir_approve] = allow
|
239
253
|
|
254
|
+
# Get the selected code block by name.
|
255
|
+
selected = mdoc.get_block_by_name(opts[:block_name])
|
256
|
+
|
257
|
+
# If approved, write the code to a file, execute it, and provide output.
|
240
258
|
if opts[:ir_approve]
|
241
|
-
write_command_file
|
242
|
-
command_execute
|
259
|
+
write_command_file(opts, required_blocks)
|
260
|
+
command_execute(opts, required_blocks.flatten.join("\n"))
|
243
261
|
save_execution_output
|
244
262
|
output_execution_summary
|
245
263
|
output_execution_result
|
@@ -473,7 +491,8 @@ module MarkdownExec
|
|
473
491
|
fcb.title = fcb.body.join(' ').gsub(/ +/, ' ')[0..64]
|
474
492
|
end
|
475
493
|
|
476
|
-
if block_given? &&
|
494
|
+
if block_given? &&
|
495
|
+
selected_messages.include?(:blocks) &&
|
477
496
|
Filter.fcb_select?(opts, fcb)
|
478
497
|
yield :blocks, fcb
|
479
498
|
end
|
@@ -496,8 +515,10 @@ module MarkdownExec
|
|
496
515
|
fcb.body = []
|
497
516
|
|
498
517
|
rest = fcb_title_groups.fetch(:rest, '')
|
499
|
-
fcb.reqs
|
500
|
-
|
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
|
501
522
|
fcb.call = rest.match(Regexp.new(opts[:block_calls_scan]))&.to_a&.first
|
502
523
|
fcb.stdin = if (tn = rest.match(/<(?<type>\$)?(?<name>[A-Za-z_-]\S+)/))
|
503
524
|
tn.named_captures.sym_keys
|
@@ -518,6 +539,27 @@ module MarkdownExec
|
|
518
539
|
end
|
519
540
|
end
|
520
541
|
|
542
|
+
def split_array(arr)
|
543
|
+
true_list = []
|
544
|
+
false_list = []
|
545
|
+
|
546
|
+
arr.each do |element|
|
547
|
+
if yield(element)
|
548
|
+
true_list << element
|
549
|
+
else
|
550
|
+
false_list << element
|
551
|
+
end
|
552
|
+
end
|
553
|
+
|
554
|
+
[true_list, false_list]
|
555
|
+
end
|
556
|
+
|
557
|
+
# # Example usage:
|
558
|
+
# array = [1, 2, 3, 4, 5]
|
559
|
+
# result = split_array(array) { |num| num.even? }
|
560
|
+
# puts "True List: #{result[0]}" # Output: True List: [2, 4]
|
561
|
+
# puts "False List: #{result[1]}" # Output: False List: [1, 3, 5]
|
562
|
+
|
521
563
|
# return body, title if option.struct
|
522
564
|
# return body if not struct
|
523
565
|
#
|
@@ -984,8 +1026,16 @@ module MarkdownExec
|
|
984
1026
|
File.write(@options[:logged_stdout_filespec], ol.join)
|
985
1027
|
end
|
986
1028
|
|
1029
|
+
# Select and execute a code block from a Markdown document.
|
1030
|
+
#
|
1031
|
+
# This method allows the user to interactively select a code block from a
|
1032
|
+
# Markdown document, obtain approval, and execute the chosen block of code.
|
1033
|
+
#
|
1034
|
+
# @param call_options [Hash] Initial options for the method.
|
1035
|
+
# @param options_block [Block] Block of options to be merged with call_options.
|
1036
|
+
# @return [Nil] Returns nil if no code block is selected or an error occurs.
|
987
1037
|
def select_approve_and_execute_block(call_options, &options_block)
|
988
|
-
opts = optsmerge
|
1038
|
+
opts = optsmerge(call_options, options_block)
|
989
1039
|
blocks_in_file = list_blocks_in_file(opts.merge(struct: true))
|
990
1040
|
mdoc = MDoc.new(blocks_in_file) do |nopts|
|
991
1041
|
opts.merge!(nopts)
|
@@ -995,7 +1045,7 @@ module MarkdownExec
|
|
995
1045
|
repeat_menu = true && !opts[:block_name].present?
|
996
1046
|
loop do
|
997
1047
|
unless opts[:block_name].present?
|
998
|
-
pt =
|
1048
|
+
pt = opts[:prompt_select_block].to_s
|
999
1049
|
|
1000
1050
|
bm = blocks_menu.map do |fcb|
|
1001
1051
|
# next if fcb.fetch(:disabled, false)
|
@@ -1017,8 +1067,7 @@ module MarkdownExec
|
|
1017
1067
|
end.compact
|
1018
1068
|
return nil if bm.count.zero?
|
1019
1069
|
|
1020
|
-
sel = prompt_with_quit
|
1021
|
-
per_page: opts[:select_page_height]
|
1070
|
+
sel = prompt_with_quit(pt, bm, per_page: opts[:select_page_height])
|
1022
1071
|
return nil if sel.nil?
|
1023
1072
|
|
1024
1073
|
## store selected option
|
@@ -1028,7 +1077,7 @@ module MarkdownExec
|
|
1028
1077
|
end.fetch(0, nil)
|
1029
1078
|
opts[:block_name] = @options[:block_name] = label_block.fetch(:name, '')
|
1030
1079
|
end
|
1031
|
-
approve_and_execute_block
|
1080
|
+
approve_and_execute_block(opts, mdoc)
|
1032
1081
|
break unless repeat_menu
|
1033
1082
|
|
1034
1083
|
opts[:block_name] = ''
|
data/lib/mdoc.rb
CHANGED
@@ -16,15 +16,47 @@ module MarkdownExec
|
|
16
16
|
class MDoc
|
17
17
|
attr_reader :table
|
18
18
|
|
19
|
-
#
|
19
|
+
# Initializes an instance of MDoc with the given table of markdown sections.
|
20
|
+
#
|
21
|
+
# @param table [Array<Hash>] An array of hashes representing markdown sections.
|
20
22
|
#
|
21
23
|
def initialize(table)
|
22
24
|
@table = table
|
23
25
|
end
|
24
26
|
|
27
|
+
# Retrieves code blocks that are required by a specified code block.
|
28
|
+
#
|
29
|
+
# @param name [String] The name of the code block to start the retrieval from.
|
30
|
+
# @return [Array<Hash>] An array of code blocks required by the specified code block.
|
31
|
+
#
|
32
|
+
def collect_recursively_required_blocks(name)
|
33
|
+
name_block = get_block_by_name(name)
|
34
|
+
raise "Named code block `#{name}` not found." if name_block.nil? || name_block.keys.empty?
|
35
|
+
|
36
|
+
all = [name_block.fetch(:name, '')] + recursively_required(name_block[:reqs])
|
37
|
+
|
38
|
+
# in order of appearance in document
|
39
|
+
# insert function blocks
|
40
|
+
@table.select { |fcb| all.include? fcb.fetch(:name, '') }
|
41
|
+
.map do |fcb|
|
42
|
+
if (call = fcb[:call])
|
43
|
+
[get_block_by_name("[#{call.match(/^%\((\S+) |\)/)[1]}]")
|
44
|
+
.merge({ cann: call })]
|
45
|
+
else
|
46
|
+
[]
|
47
|
+
end + [fcb]
|
48
|
+
end.flatten(1)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Collects recursively required code blocks and returns them as an array of strings.
|
52
|
+
#
|
53
|
+
# @param name [String] The name of the code block to start the collection from.
|
54
|
+
# @return [Array<String>] An array of strings containing the collected code blocks.
|
55
|
+
#
|
25
56
|
def collect_recursively_required_code(name)
|
26
|
-
|
27
|
-
|
57
|
+
collect_wrapped_blocks(
|
58
|
+
collect_recursively_required_blocks(name)
|
59
|
+
).map do |fcb|
|
28
60
|
body = fcb[:body].join("\n")
|
29
61
|
|
30
62
|
if fcb[:cann]
|
@@ -58,43 +90,37 @@ module MarkdownExec
|
|
58
90
|
end.flatten(1)
|
59
91
|
end
|
60
92
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
[]
|
80
|
-
end + [fcb]
|
81
|
-
end.flatten(1)
|
82
|
-
end
|
83
|
-
|
84
|
-
# :reek:UtilityFunction
|
85
|
-
def hide_menu_block_per_options(opts, block)
|
86
|
-
(opts[:hide_blocks_by_name] &&
|
87
|
-
block[:name]&.match(Regexp.new(opts[:block_name_hidden_match])) &&
|
88
|
-
(block[:name]&.present? || block[:label]&.present?)
|
89
|
-
)
|
93
|
+
# Retrieves code blocks that are wrapped
|
94
|
+
# wraps are applied from left to right
|
95
|
+
# e.g. w1 w2 => w1-before w2-before w1 w2 w2-after w1-after
|
96
|
+
#
|
97
|
+
# @return [Array<Hash>] An array of code blocks required by the specified code blocks.
|
98
|
+
#
|
99
|
+
def collect_wrapped_blocks(blocks)
|
100
|
+
blocks.map do |block|
|
101
|
+
(block[:wraps] || []).map do |wrap|
|
102
|
+
wrap_before = wrap.sub('}', '-before}') ### hardcoded wrap name
|
103
|
+
@table.select { |fcb| [wrap_before, wrap].include? fcb[:name] }
|
104
|
+
end.flatten(1) +
|
105
|
+
[block] +
|
106
|
+
(block[:wraps] || []).reverse.map do |wrap|
|
107
|
+
wrap_after = wrap.sub('}', '-after}') ### hardcoded wrap name
|
108
|
+
@table.select { |fcb| fcb[:name] == wrap_after }
|
109
|
+
end.flatten(1)
|
110
|
+
end.flatten(1).compact
|
90
111
|
end
|
91
112
|
|
113
|
+
# Retrieves code blocks based on the provided options.
|
114
|
+
#
|
115
|
+
# @param opts [Hash] The options used for filtering code blocks.
|
116
|
+
# @return [Array<Hash>] An array of code blocks that match the options.
|
117
|
+
#
|
92
118
|
def fcbs_per_options(opts = {})
|
93
119
|
options = opts.merge(block_name_hidden_match: nil)
|
94
120
|
selrows = @table.select do |fcb_title_groups|
|
95
121
|
Filter.fcb_select? options, fcb_title_groups
|
96
122
|
end
|
97
|
-
|
123
|
+
# pp selrows; binding.pry
|
98
124
|
### hide rows correctly
|
99
125
|
|
100
126
|
if opts[:hide_blocks_by_name]
|
@@ -107,6 +133,40 @@ module MarkdownExec
|
|
107
133
|
end
|
108
134
|
end
|
109
135
|
|
136
|
+
# Retrieves a code block by its name.
|
137
|
+
#
|
138
|
+
# @param name [String] The name of the code block to retrieve.
|
139
|
+
# @param default [Hash] The default value to return if the code block is not found.
|
140
|
+
# @return [Hash] The code block as a hash or the default value if not found.
|
141
|
+
#
|
142
|
+
def get_block_by_name(name, default = {})
|
143
|
+
@table.select { |fcb| fcb.fetch(:name, '') == name }.fetch(0, default)
|
144
|
+
end
|
145
|
+
|
146
|
+
# Checks if a code block should be hidden based on the given options.
|
147
|
+
#
|
148
|
+
# @param opts [Hash] The options used for hiding code blocks.
|
149
|
+
# @param block [Hash] The code block to check for hiding.
|
150
|
+
# @return [Boolean] True if the code block should be hidden; false otherwise.
|
151
|
+
#
|
152
|
+
# :reek:UtilityFunction
|
153
|
+
def hide_menu_block_per_options(opts, block)
|
154
|
+
(opts[:hide_blocks_by_name] &&
|
155
|
+
((opts[:block_name_hidden_match]&.present? &&
|
156
|
+
block[:name]&.match(Regexp.new(opts[:block_name_hidden_match]))) ||
|
157
|
+
(opts[:block_name_include_match]&.present? &&
|
158
|
+
block[:name]&.match(Regexp.new(opts[:block_name_include_match]))) ||
|
159
|
+
(opts[:block_name_wrapper_match]&.present? &&
|
160
|
+
block[:name]&.match(Regexp.new(opts[:block_name_wrapper_match])))) &&
|
161
|
+
(block[:name]&.present? || block[:label]&.present?)
|
162
|
+
)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Recursively fetches required code blocks for a given list of requirements.
|
166
|
+
#
|
167
|
+
# @param reqs [Array<String>] An array of requirements to start the recursion from.
|
168
|
+
# @return [Array<String>] An array of recursively required code block names.
|
169
|
+
#
|
110
170
|
def recursively_required(reqs)
|
111
171
|
return [] unless reqs
|
112
172
|
|
@@ -128,14 +188,11 @@ module MarkdownExec
|
|
128
188
|
end
|
129
189
|
|
130
190
|
if $PROGRAM_NAME == __FILE__
|
131
|
-
|
132
|
-
|
191
|
+
require 'bundler/setup'
|
192
|
+
Bundler.require(:default)
|
133
193
|
|
134
194
|
require 'minitest/autorun'
|
135
195
|
|
136
|
-
require_relative 'tap'
|
137
|
-
include Tap
|
138
|
-
|
139
196
|
module MarkdownExec
|
140
197
|
class TestMDoc < Minitest::Test
|
141
198
|
def setup
|
@@ -161,12 +218,14 @@ if $PROGRAM_NAME == __FILE__
|
|
161
218
|
assert_equal({}, result_missing)
|
162
219
|
end
|
163
220
|
|
164
|
-
def
|
165
|
-
result = @doc.
|
221
|
+
def test_collect_recursively_required_blocks
|
222
|
+
result = @doc.collect_recursively_required_blocks('block3')
|
166
223
|
expected_result = [@table[0], @table[1], @table[2]]
|
167
224
|
assert_equal expected_result, result
|
168
225
|
|
169
|
-
assert_raises(RuntimeError)
|
226
|
+
assert_raises(RuntimeError) do
|
227
|
+
@doc.collect_recursively_required_blocks('missing_block')
|
228
|
+
end
|
170
229
|
end
|
171
230
|
|
172
231
|
def test_hide_menu_block_per_options
|
@@ -190,5 +249,58 @@ if $PROGRAM_NAME == __FILE__
|
|
190
249
|
assert_equal [], result_no_reqs
|
191
250
|
end
|
192
251
|
end
|
252
|
+
|
253
|
+
class TestMDoc2 < Minitest::Test
|
254
|
+
# Mocking the @table object for testing
|
255
|
+
def setup
|
256
|
+
@table = [
|
257
|
+
{ name: '{wrap1}' },
|
258
|
+
{ name: '{wrap2-before}' },
|
259
|
+
{ name: '{wrap2}' },
|
260
|
+
{ name: '{wrap2-after}' },
|
261
|
+
{ name: '{wrap3-before}' },
|
262
|
+
{ name: '{wrap3}' },
|
263
|
+
{ name: '{wrap3-after}' }
|
264
|
+
]
|
265
|
+
@mdoc = MDoc.new(@table)
|
266
|
+
end
|
267
|
+
|
268
|
+
def test_collect_wrapped_blocks
|
269
|
+
# Test case 1: blocks with wraps
|
270
|
+
assert_equal(%w[{wrap1} a],
|
271
|
+
@mdoc.collect_wrapped_blocks(
|
272
|
+
[{ name: 'a',
|
273
|
+
wraps: ['{wrap1}'] }]
|
274
|
+
).map do |block|
|
275
|
+
block[:name]
|
276
|
+
end)
|
277
|
+
|
278
|
+
assert_equal(%w[{wrap2-before} {wrap2} b {wrap2-after}],
|
279
|
+
@mdoc.collect_wrapped_blocks(
|
280
|
+
[{ name: 'b',
|
281
|
+
wraps: ['{wrap2}'] }]
|
282
|
+
).map do |block|
|
283
|
+
block[:name]
|
284
|
+
end)
|
285
|
+
|
286
|
+
assert_equal(%w[{wrap2-before} {wrap2} {wrap3-before} {wrap3} c {wrap3-after} {wrap2-after}],
|
287
|
+
@mdoc.collect_wrapped_blocks(
|
288
|
+
[{ name: 'c',
|
289
|
+
wraps: %w[{wrap2} {wrap3}] }]
|
290
|
+
).map { |block| block[:name] })
|
291
|
+
|
292
|
+
# Test case 2: blocks with no wraps
|
293
|
+
blocks = @mdoc.collect_wrapped_blocks([])
|
294
|
+
assert_empty blocks
|
295
|
+
|
296
|
+
# Test case 3: blocks with missing wraps
|
297
|
+
assert_equal(
|
298
|
+
%w[block4],
|
299
|
+
@mdoc.collect_wrapped_blocks([{ name: 'block4', wraps: ['wrap4'] }]).map do |block|
|
300
|
+
block[:name]
|
301
|
+
end
|
302
|
+
)
|
303
|
+
end
|
304
|
+
end
|
193
305
|
end
|
194
306
|
end
|
data/lib/menu.src.yml
CHANGED
@@ -266,15 +266,25 @@
|
|
266
266
|
:env_var: MDE_SELECT_BY_SHELL_REGEX
|
267
267
|
:opt_name: select_by_shell_regex
|
268
268
|
:procname: val_as_str
|
269
|
-
- :default: "
|
269
|
+
- :default: "^-.+-$"
|
270
270
|
:description: Pattern for blocks to hide from user-selection
|
271
271
|
:env_var: MDE_BLOCK_NAME_HIDDEN_MATCH
|
272
272
|
:opt_name: block_name_hidden_match
|
273
273
|
:procname: val_as_str
|
274
|
+
- :default: "^[\\(\\[].*[\\)\\]]$"
|
275
|
+
:description: Pattern for blocks to hide from user-selection
|
276
|
+
:env_var: MDE_BLOCK_NAME_INCLUDE_MATCH
|
277
|
+
:opt_name: block_name_include_match
|
278
|
+
:procname: val_as_str
|
274
279
|
- :default: ":(?<title>\\S+)( |$)"
|
275
280
|
:env_var: MDE_BLOCK_NAME_MATCH
|
276
281
|
:opt_name: block_name_match
|
277
282
|
:procname: val_as_str
|
283
|
+
- :default: "^{.+}$"
|
284
|
+
:description: Pattern for block names to use as wrappers
|
285
|
+
:env_var: MDE_BLOCK_NAME_WRAPPER_MATCH
|
286
|
+
:opt_name: block_name_wrapper_match
|
287
|
+
:procname: val_as_str
|
278
288
|
- :default: "%\\([^\\)]+\\)"
|
279
289
|
:env_var: MDE_BLOCK_CALLS_SCAN
|
280
290
|
:opt_name: block_calls_scan
|
@@ -357,7 +367,7 @@
|
|
357
367
|
:env_var: MDE_MENU_INITIAL_DIVIDER
|
358
368
|
:opt_name: menu_initial_divider
|
359
369
|
:procname: val_as_str
|
360
|
-
- :default: "
|
370
|
+
- :default: "%{name} [%{status}]"
|
361
371
|
:description: format for menu tasks and demarcations
|
362
372
|
:env_var: MDE_MENU_TASK_FORMAT
|
363
373
|
:opt_name: menu_task_format
|
data/lib/menu.yml
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# MDE - Markdown Executor (1.3.
|
1
|
+
# MDE - Markdown Executor (1.3.8)
|
2
2
|
---
|
3
3
|
- :arg_name: NAME
|
4
4
|
:compreply: false
|
@@ -267,15 +267,25 @@
|
|
267
267
|
:env_var: MDE_SELECT_BY_SHELL_REGEX
|
268
268
|
:opt_name: select_by_shell_regex
|
269
269
|
:procname: val_as_str
|
270
|
-
- :default: "
|
270
|
+
- :default: "^-.+-$"
|
271
271
|
:description: Pattern for blocks to hide from user-selection
|
272
272
|
:env_var: MDE_BLOCK_NAME_HIDDEN_MATCH
|
273
273
|
:opt_name: block_name_hidden_match
|
274
274
|
:procname: val_as_str
|
275
|
+
- :default: "^[\\(\\[].*[\\)\\]]$"
|
276
|
+
:description: Pattern for blocks to hide from user-selection
|
277
|
+
:env_var: MDE_BLOCK_NAME_INCLUDE_MATCH
|
278
|
+
:opt_name: block_name_include_match
|
279
|
+
:procname: val_as_str
|
275
280
|
- :default: ":(?<title>\\S+)( |$)"
|
276
281
|
:env_var: MDE_BLOCK_NAME_MATCH
|
277
282
|
:opt_name: block_name_match
|
278
283
|
:procname: val_as_str
|
284
|
+
- :default: "^{.+}$"
|
285
|
+
:description: Pattern for block names to use as wrappers
|
286
|
+
:env_var: MDE_BLOCK_NAME_WRAPPER_MATCH
|
287
|
+
:opt_name: block_name_wrapper_match
|
288
|
+
:procname: val_as_str
|
279
289
|
- :default: "%\\([^\\)]+\\)"
|
280
290
|
:env_var: MDE_BLOCK_CALLS_SCAN
|
281
291
|
:opt_name: block_calls_scan
|
@@ -358,7 +368,7 @@
|
|
358
368
|
:env_var: MDE_MENU_INITIAL_DIVIDER
|
359
369
|
:opt_name: menu_initial_divider
|
360
370
|
:procname: val_as_str
|
361
|
-
- :default: "
|
371
|
+
- :default: "%{name} [%{status}]"
|
362
372
|
:description: format for menu tasks and demarcations
|
363
373
|
:env_var: MDE_MENU_TASK_FORMAT
|
364
374
|
:opt_name: menu_task_format
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: markdown_exec
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.3.
|
4
|
+
version: 1.3.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Fareed Stevenson
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-10-
|
11
|
+
date: 2023-10-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: clipboard
|