markdown_exec 2.8.5 → 3.0.0

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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -1
  3. data/Gemfile.lock +1 -1
  4. data/Rakefile +0 -33
  5. data/bats/bats.bats +2 -0
  6. data/bats/block-type-link.bats +1 -1
  7. data/bats/block-type-ux-allowed.bats +2 -2
  8. data/bats/block-type-ux-invalid.bats +1 -1
  9. data/bats/{block-type-ux-preconditions.bats → block-type-ux-required-variables.bats} +1 -1
  10. data/bats/block-type-ux-row-format.bats +1 -1
  11. data/bats/block-type-ux-sources.bats +36 -0
  12. data/bats/border.bats +1 -1
  13. data/bats/cli.bats +2 -2
  14. data/bats/command-substitution-options.bats +14 -0
  15. data/bats/command-substitution.bats +1 -1
  16. data/bats/fail.bats +5 -2
  17. data/bats/indented-block-type-vars.bats +1 -1
  18. data/bats/markup.bats +1 -1
  19. data/bats/option-expansion.bats +8 -0
  20. data/bats/table-column-truncate.bats +1 -1
  21. data/bats/test_helper.bash +50 -5
  22. data/docs/dev/bats-document-configuration.md +1 -1
  23. data/docs/dev/block-type-ux-allowed.md +5 -7
  24. data/docs/dev/block-type-ux-auto.md +8 -5
  25. data/docs/dev/block-type-ux-chained.md +4 -2
  26. data/docs/dev/block-type-ux-echo-hash.md +6 -7
  27. data/docs/dev/block-type-ux-echo.md +2 -2
  28. data/docs/dev/block-type-ux-exec.md +3 -5
  29. data/docs/dev/block-type-ux-hidden.md +3 -0
  30. data/docs/dev/{block-type-ux-preconditions.md → block-type-ux-required-variables.md} +1 -2
  31. data/docs/dev/block-type-ux-row-format.md +3 -4
  32. data/docs/dev/block-type-ux-sources.md +57 -0
  33. data/docs/dev/block-type-ux-transform.md +0 -4
  34. data/docs/dev/command-substitution-options.md +61 -0
  35. data/docs/dev/indented-block-type-vars.md +1 -0
  36. data/docs/dev/menu-pagination-indent.md +123 -0
  37. data/docs/dev/menu-pagination.md +111 -0
  38. data/docs/dev/option-expansion.md +10 -0
  39. data/lib/ansi_formatter.rb +2 -0
  40. data/lib/block_cache.rb +197 -0
  41. data/lib/command_result.rb +57 -0
  42. data/lib/constants.rb +18 -0
  43. data/lib/error_reporting.rb +38 -0
  44. data/lib/evaluate_shell_expressions.rb +43 -18
  45. data/lib/fcb.rb +98 -7
  46. data/lib/hash_delegator.rb +526 -322
  47. data/lib/markdown_exec/version.rb +1 -1
  48. data/lib/markdown_exec.rb +136 -45
  49. data/lib/mdoc.rb +59 -10
  50. data/lib/menu.src.yml +23 -11
  51. data/lib/menu.yml +22 -12
  52. data/lib/value_or_exception.rb +76 -0
  53. metadata +16 -4
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env -S bundle exec ruby
2
+ # frozen_string_literal: true
3
+
4
+ # encoding=utf-8
5
+
6
+ # Encapsulates the result of executing a system command, storing its output,
7
+ # exit status, and any number of additional, arbitrary attributes.
8
+ #
9
+ # @example
10
+ # result = CommandResult.new(stdout: output, exit_status: $?.exitstatus, duration: 1.23)
11
+ # result.stdout # => output
12
+ # result.exit_status # => 0
13
+ # result.duration # => 1.23
14
+ # result.new_field = 42
15
+ # result.new_field # => 42
16
+ # result.success? # => true
17
+ class CommandResult
18
+ # @param attributes [Hash{Symbol=>Object}] initial named attributes
19
+ def initialize(**attributes)
20
+ @attributes = {}
21
+ @attributes[:exit_status] = 0
22
+ @attributes[:stdout] = ''
23
+ attributes.each { |name, value| @attributes[name] = value }
24
+ end
25
+
26
+ def failure?
27
+ !success?
28
+ end
29
+
30
+ # @return [Boolean] true if the exit status is zero
31
+ def success?
32
+ exit_status.zero?
33
+ end
34
+
35
+ def method_missing(name, *args)
36
+ key = name.to_s.chomp('=').to_sym
37
+
38
+ if name.to_s.end_with?('=') # setter
39
+ @attributes[key] = args.first
40
+ elsif attribute?(name) # getter
41
+ @attributes[name]
42
+ else
43
+ super
44
+ end
45
+ end
46
+
47
+ def respond_to_missing?(name, include_private = false)
48
+ key = name.to_s.chomp('=').to_sym
49
+ attribute?(key) || super
50
+ end
51
+
52
+ private
53
+
54
+ def attribute?(name)
55
+ @attributes.key?(name)
56
+ end
57
+ end
data/lib/constants.rb CHANGED
@@ -125,3 +125,21 @@ class TtyMenu
125
125
  ENABLE = nil
126
126
  DISABLE = ''
127
127
  end
128
+
129
+ class ExportValueSource
130
+ ALLOW = ':allow'
131
+ DEFAULT = ':default'
132
+ ECHO = ':echo'
133
+ EDIT = ':edit'
134
+ EXEC = ':exec'
135
+ FALSE = false
136
+ end
137
+
138
+ class UxActSource
139
+ ALLOW = :allow
140
+ DEFAULT = :default
141
+ ECHO = :echo
142
+ EDIT = :edit
143
+ EXEC = :exec
144
+ FALSE = 'false'
145
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # encoding=utf-8
4
+
5
+ ##
6
+ # Module providing standardized error‐reporting:
7
+ # logs either an Exception’s details or a simple
8
+ # string message—optionally with context—and then
9
+ # re‐raises either the original Exception or a new
10
+ # RuntimeError for string messages.
11
+ #
12
+ # Including this module gives you:
13
+ # • instance method → report_and_reraise(...)
14
+ # • class method → report_and_reraise(...)
15
+ module ErrorReporting
16
+ def self.included(base)
17
+ base.extend(self)
18
+ end
19
+
20
+ def report_and_reraise(error_or_message, context: nil)
21
+ if error_or_message.is_a?(Exception)
22
+ header = +"#{error_or_message.class}: #{error_or_message.message}"
23
+ header << " (#{context})" if context
24
+
25
+ ww header
26
+ ww error_or_message.backtrace.join("\n") if error_or_message.backtrace
27
+
28
+ raise error_or_message
29
+ else
30
+ header = +error_or_message.to_s
31
+ header << " (#{context})" if context
32
+
33
+ ww header
34
+
35
+ raise error_or_message.to_s
36
+ end
37
+ end
38
+ end
@@ -10,12 +10,11 @@ class EvaluateShellExpression
10
10
  end
11
11
 
12
12
  def evaluate_shell_expressions(initial_code, expressions, shell: '/bin/bash',
13
- key_format: '%%<%s>',
14
- initial_code_required: false)
13
+ initial_code_required: false,
14
+ occurrence_expressions: nil)
15
15
  # !!p initial_code expressions key_format shell
16
16
  return if (initial_code_required && (initial_code.nil? || initial_code.empty?)) ||
17
- expressions.nil? || expressions.empty? ||
18
- key_format.nil? || key_format.empty?
17
+ expressions.nil? || expressions.empty?
19
18
 
20
19
  # token to separate output
21
20
  token = "__TOKEN__#{Time.now.to_i}__"
@@ -28,7 +27,7 @@ def evaluate_shell_expressions(initial_code, expressions, shell: '/bin/bash',
28
27
  end
29
28
 
30
29
  # Execute
31
- stdout_str, stderr_str, status = Open3.capture3(shell, '-c', script)
30
+ stdout_str, _, status = Open3.capture3(shell, '-c', script)
32
31
 
33
32
  unless status.success?
34
33
  return EvaluateShellExpression::StatusFail
@@ -40,7 +39,7 @@ def evaluate_shell_expressions(initial_code, expressions, shell: '/bin/bash',
40
39
  unless part.empty?
41
40
  part[1..-1].tap do |output_parts|
42
41
  expressions.each_with_index do |(key, _expression), index|
43
- result_hash[format(key_format, key)] = output_parts[index].chomp
42
+ result_hash[occurrence_expressions[key]] = output_parts[index].chomp
44
43
  end
45
44
  end
46
45
  end
@@ -54,7 +53,6 @@ require 'bundler/setup'
54
53
  Bundler.require(:default)
55
54
 
56
55
  require 'minitest/autorun'
57
- require 'open3'
58
56
 
59
57
  class TestShellExpressionEvaluator < Minitest::Test
60
58
  def setup
@@ -66,7 +64,11 @@ class TestShellExpressionEvaluator < Minitest::Test
66
64
 
67
65
  def test_single_expression
68
66
  expressions = { 'greeting' => "echo 'Hello, World!'" }
69
- result = evaluate_shell_expressions(@initial_code, expressions)
67
+ occurrence_expressions = { 'greeting' => '%<greeting>' }
68
+ result = evaluate_shell_expressions(
69
+ @initial_code, expressions,
70
+ occurrence_expressions: occurrence_expressions
71
+ )
70
72
 
71
73
  assert_equal 'Hello, World!', result['%<greeting>']
72
74
  end
@@ -77,7 +79,15 @@ class TestShellExpressionEvaluator < Minitest::Test
77
79
  'date' => 'date +%Y-%m-%d',
78
80
  'kernel' => 'uname -r'
79
81
  }
80
- result = evaluate_shell_expressions(@initial_code, expressions)
82
+ occurrence_expressions = {
83
+ 'date' => '%<date>',
84
+ 'greeting' => '%<greeting>',
85
+ 'kernel' => '%<kernel>'
86
+ }
87
+ result = evaluate_shell_expressions(
88
+ @initial_code, expressions,
89
+ occurrence_expressions: occurrence_expressions
90
+ )
81
91
 
82
92
  assert_equal 'Hello, World!', result['%<greeting>']
83
93
  assert_match(/\d{4}-\d{2}-\d{2}/, result['%<date>'])
@@ -86,15 +96,22 @@ class TestShellExpressionEvaluator < Minitest::Test
86
96
 
87
97
  def test_empty_expressions_list
88
98
  expressions = {}
89
- result = evaluate_shell_expressions(@initial_code, expressions)
99
+ occurrence_expressions = {}
100
+ result = evaluate_shell_expressions(
101
+ @initial_code, expressions,
102
+ occurrence_expressions: occurrence_expressions
103
+ )
90
104
 
91
105
  assert_nil result
92
106
  end
93
107
 
94
108
  def test_invalid_expression
95
109
  expressions = { 'invalid' => 'invalid_command' }
96
-
97
- result = evaluate_shell_expressions(@initial_code, expressions)
110
+ occurrence_expressions = {}
111
+ result = evaluate_shell_expressions(
112
+ @initial_code, expressions,
113
+ occurrence_expressions: occurrence_expressions
114
+ )
98
115
 
99
116
  assert_equal EvaluateShellExpression::StatusFail, result
100
117
  end
@@ -105,18 +122,26 @@ class TestShellExpressionEvaluator < Minitest::Test
105
122
  echo "Custom setup message"
106
123
  BASH
107
124
  expressions = { 'test' => 'echo Test after initial setup' }
108
-
109
- result = evaluate_shell_expressions(initial_code, expressions)
125
+ occurrence_expressions = { 'test' => '%<test>' }
126
+ result = evaluate_shell_expressions(
127
+ @initial_code, expressions,
128
+ occurrence_expressions: occurrence_expressions
129
+ )
110
130
 
111
131
  assert_equal 'Test after initial setup', result['%<test>']
112
132
  end
113
133
 
114
134
  def test_large_number_of_expressions
115
- expressions = (1..100).map do |i|
135
+ expressions = (1..100).to_h do |i|
116
136
  ["expr_#{i}", "echo Expression #{i}"]
117
- end.to_h
118
-
119
- result = evaluate_shell_expressions(@initial_code, expressions)
137
+ end
138
+ occurrence_expressions = (1..100).to_h do |i|
139
+ ["expr_#{i}", "%<expr_#{i}>"]
140
+ end
141
+ result = evaluate_shell_expressions(
142
+ @initial_code, expressions,
143
+ occurrence_expressions: occurrence_expressions
144
+ )
120
145
 
121
146
  expressions.each_with_index do |(key, _expression), index|
122
147
  assert_equal "Expression #{index + 1}", result["%<#{key}>"]
data/lib/fcb.rb CHANGED
@@ -4,7 +4,7 @@
4
4
  # encoding=utf-8
5
5
  require 'digest'
6
6
  require_relative 'namer'
7
-
7
+ BT_UX_FLD_REQUIRED = 'required'
8
8
  def parse_yaml_of_ux_block(
9
9
  data,
10
10
  menu_format: nil,
@@ -18,15 +18,17 @@ def parse_yaml_of_ux_block(
18
18
  raise "Name is missing in UX block: #{data.inspect}" unless name.present?
19
19
 
20
20
  OpenStruct.new(
21
- allowed: export['allowed'],
21
+ act: export['act'],
22
+ allow: export['allow'] || export['allowed'],
22
23
  default: export['default'],
23
24
  echo: export['echo'],
24
25
  exec: export['exec'],
25
- menu_format: export['menu_format'] || menu_format,
26
+ init: export['init'],
27
+ menu_format: export['format'] || export['menu_format'] || menu_format,
26
28
  name: name,
27
- preconditions: export['preconditions'],
28
29
  prompt: export['prompt'] || prompt,
29
30
  readonly: export['readonly'].nil? ? false : export['readonly'],
31
+ required: export['require'] || export['required'] || export[BT_UX_FLD_REQUIRED],
30
32
  transform: export['transform'],
31
33
  validate: export['validate'] || validate
32
34
  )
@@ -137,8 +139,10 @@ module MarkdownExec
137
139
  )&.fetch(1, nil)
138
140
  titlexcall = call ? @attrs[:title].sub("%#{call}", '') : @attrs[:title]
139
141
 
140
- oname = if block_name_nick_match.present? &&
141
- @attrs[:oname] =~ Regexp.new(block_name_nick_match)
142
+ oname = if is_split?
143
+ @attrs[:text]
144
+ elsif block_name_nick_match.present? &&
145
+ @attrs[:oname] =~ Regexp.new(block_name_nick_match)
142
146
  @attrs[:nickname] = $~[0]
143
147
  derive_title_from_body
144
148
  else
@@ -185,7 +189,61 @@ module MarkdownExec
185
189
  def self.format_multiline_body_as_title(body_lines)
186
190
  body_lines.map.with_index do |line, index|
187
191
  index.zero? ? line : " #{line}"
188
- end.join("\n") << "\n"
192
+ end.join("\n")
193
+ end
194
+
195
+ def self.act_source(export)
196
+ # If `false`, the UX block is not activated.
197
+ # If one of `:allow`, `:echo`, `:edit`, or `:exec` is specified,
198
+ # the value is calculated or the user is prompted.
199
+ # If not present, the default value is `:edit`.
200
+ if export.act.nil?
201
+ export.act = if export.init.to_s == 'false'
202
+ if export.allow.present?
203
+ UxActSource::ALLOW
204
+ elsif export.echo.present?
205
+ UxActSource::ECHO
206
+ elsif export.edit.present?
207
+ UxActSource::EDIT
208
+ elsif export.exec.present?
209
+ UxActSource::EXEC
210
+ else
211
+ UxActSource::EDIT
212
+ end
213
+ elsif export.allow.present?
214
+ UxActSource::ALLOW
215
+ else
216
+ UxActSource::EDIT
217
+ end
218
+ end
219
+
220
+ export.act
221
+ end
222
+
223
+ def self.init_source(export)
224
+ # If `false`, there is no initial value set.
225
+ # If a string, it is the initial value of the object variable.
226
+ # Otherwise, if one of `:allow`, `:echo`, or `:exec` is specified,
227
+ # the value is the output of the `echo` or `exec` evaluation
228
+ # or the first allowed value.
229
+ # If not present, the default value is whichever of
230
+ # `:allow`, `:default`, `:echo`, or `:exec` is present.
231
+ if export.init.nil?
232
+ export.init = case
233
+ when export.allow.present?
234
+ UxActSource::ALLOW
235
+ when export.default.present?
236
+ UxActSource::DEFAULT
237
+ when export.echo.present?
238
+ UxActSource::ECHO
239
+ when export.exec.present?
240
+ UxActSource::EXEC
241
+ else
242
+ UxActSource::FALSE
243
+ end
244
+ end
245
+
246
+ export.init
189
247
  end
190
248
 
191
249
  # :reek:ManualDispatch
@@ -198,6 +256,14 @@ module MarkdownExec
198
256
  dependency_names.include?(@attrs[:s2title])
199
257
  end
200
258
 
259
+ def is_disabled?
260
+ @attrs[:disabled] == TtyMenu::DISABLE
261
+ end
262
+
263
+ def is_enabled?
264
+ !is_disabled?
265
+ end
266
+
201
267
  def is_named?(name)
202
268
  @attrs[:dname] == name ||
203
269
  @attrs[:nickname] == name ||
@@ -206,6 +272,31 @@ module MarkdownExec
206
272
  @attrs[:s2title] == name
207
273
  end
208
274
 
275
+ # true if this is a line split block
276
+ def is_split?
277
+ is_split_first? || is_split_rest?
278
+ end
279
+
280
+ # true if this block displays its split body
281
+ # names and nicknames are displayed instead of the body
282
+ # ux blocks display a single line for the named variable
283
+ # split blocks are: opts, shell, vars
284
+ def is_split_displayed?(opts)
285
+ @attrs[:type] != BlockType::UX &&
286
+ !(@attrs[:start_line] =~ Regexp.new(opts[:block_name_nick_match]) ||
287
+ @attrs[:start_line] =~ Regexp.new(opts[:block_name_match]))
288
+ end
289
+
290
+ # true if this is the first line in a split block
291
+ def is_split_first?
292
+ @attrs.fetch(:is_split_first, false)
293
+ end
294
+
295
+ # true if this is the second or later line in a split block
296
+ def is_split_rest?
297
+ @attrs.fetch(:is_split_rest, false)
298
+ end
299
+
209
300
  # :reek:ManualDispatch
210
301
  def method_missing(method, *args, &block)
211
302
  method_name = method.to_s