markdown_exec 2.2.0 → 2.4.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +16 -4
  3. data/CHANGELOG.md +28 -0
  4. data/Gemfile.lock +1 -1
  5. data/Rakefile +32 -8
  6. data/bats/bats.bats +33 -0
  7. data/bats/block-types.bats +56 -0
  8. data/bats/cli.bats +74 -0
  9. data/bats/fail.bats +11 -0
  10. data/bats/history.bats +34 -0
  11. data/bats/markup.bats +66 -0
  12. data/bats/mde.bats +29 -0
  13. data/bats/options.bats +92 -0
  14. data/bats/test_helper.bash +152 -0
  15. data/bin/tab_completion.sh +44 -20
  16. data/docs/dev/block-type-opts.md +10 -0
  17. data/docs/dev/block-type-port.md +24 -0
  18. data/docs/dev/block-type-vars.md +7 -0
  19. data/docs/dev/pass-through-arguments.md +8 -0
  20. data/docs/dev/specs-import.md +9 -0
  21. data/docs/dev/specs.md +83 -0
  22. data/docs/dev/text-decoration.md +7 -0
  23. data/examples/bash-blocks.md +4 -4
  24. data/examples/block-names.md +40 -5
  25. data/examples/import0.md +23 -0
  26. data/examples/import1.md +13 -0
  27. data/examples/link-blocks-vars.md +3 -3
  28. data/examples/opts-blocks-require.md +6 -6
  29. data/examples/table-markup.md +31 -0
  30. data/examples/text-markup.md +58 -0
  31. data/examples/vars-blocks.md +2 -2
  32. data/examples/wrap.md +87 -9
  33. data/lib/ansi_formatter.rb +12 -6
  34. data/lib/ansi_string.rb +153 -0
  35. data/lib/argument_processor.rb +160 -0
  36. data/lib/cached_nested_file_reader.rb +4 -2
  37. data/lib/ce_get_cost_and_usage.rb +4 -3
  38. data/lib/cli.rb +1 -1
  39. data/lib/colorize.rb +41 -0
  40. data/lib/constants.rb +17 -0
  41. data/lib/directory_searcher.rb +4 -2
  42. data/lib/doh.rb +190 -0
  43. data/lib/env.rb +1 -1
  44. data/lib/exceptions.rb +9 -6
  45. data/lib/fcb.rb +0 -199
  46. data/lib/filter.rb +18 -5
  47. data/lib/find_files.rb +8 -3
  48. data/lib/format_table.rb +406 -0
  49. data/lib/hash_delegator.rb +939 -611
  50. data/lib/hierarchy_string.rb +221 -0
  51. data/lib/input_sequencer.rb +19 -11
  52. data/lib/instance_method_wrapper.rb +2 -1
  53. data/lib/layered_hash.rb +143 -0
  54. data/lib/link_history.rb +22 -8
  55. data/lib/markdown_exec/version.rb +1 -1
  56. data/lib/markdown_exec.rb +420 -165
  57. data/lib/mdoc.rb +38 -38
  58. data/lib/menu.src.yml +832 -680
  59. data/lib/menu.yml +814 -689
  60. data/lib/namer.rb +6 -12
  61. data/lib/object_present.rb +1 -1
  62. data/lib/option_value.rb +7 -3
  63. data/lib/poly.rb +33 -14
  64. data/lib/resize_terminal.rb +60 -52
  65. data/lib/saved_assets.rb +45 -34
  66. data/lib/saved_files_matcher.rb +6 -3
  67. data/lib/streams_out.rb +7 -1
  68. data/lib/table_extractor.rb +166 -0
  69. data/lib/tap.rb +5 -6
  70. data/lib/text_analyzer.rb +236 -0
  71. metadata +28 -3
  72. data/lib/std_out_err_logger.rb +0 -119
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ansi_string'
4
+
5
+ # Class representing a hierarchy of substrings stored as Hash nodes
6
+ # HierarchyString is a class that represents and manipulates strings based on a hierarchical structure.
7
+ # The input to the class can be a single hash or an array of nested hashes, where each hash contains a
8
+ # text string and an optional decoration or transformation (like `:downcase`, `:upcase`, etc.).
9
+ #
10
+ # The primary functionalities of the class include:
11
+ #
12
+ # - **Initialization**: The class can be initialized with either a single hash or an array of nested hashes.
13
+ # Each hash contains a @text_sym key representing the string and a @style_sym key representing the transformation
14
+ # (optional).
15
+ #
16
+ # - **Concatenation**: The `concatenate` method concatenates all text strings in the hierarchy into a single string.
17
+ #
18
+ # - **Decoration**: The `decorate` method applies the specified transformation (like `:downcase`, `:upcase`) to the
19
+ # text in the hierarchy and returns the decorated string.
20
+ #
21
+ # - **Text Replacement**: The `replace_text!` method allows in-place replacement of text in the hierarchy by applying
22
+ # a block to each text string.
23
+ #
24
+ # - **Method Delegation**: The class uses `method_missing` and `respond_to_missing?` to delegate undefined method calls
25
+ # to the string object, allowing for dynamic method handling on the concatenated string (e.g., `capitalize`).
26
+ #
27
+ # This class is useful for situations where strings are represented in a hierarchical or nested structure and need
28
+ # to be manipulated or transformed in a consistent and customizable manner.
29
+ class HierarchyString
30
+ attr_accessor :substrings
31
+
32
+ # Initialize with a single hash or an array of hashes
33
+ def initialize(substrings, text_sym: :text, style_sym: :color)
34
+ @substrings = parse_substrings(substrings)
35
+ @text_sym = text_sym
36
+ @style_sym = style_sym
37
+ end
38
+
39
+ def map_substring_text_yield(tree, &block)
40
+ case tree
41
+ when Array
42
+ tree.each.with_index do |node, ind|
43
+ case node
44
+ when String
45
+ tree[ind] = yield node
46
+ else
47
+ map_substring_text_yield(node, &block)
48
+ end
49
+ end
50
+ when Hash
51
+ text = yield tree[@text_sym]
52
+ tree[@text_sym] = text
53
+
54
+ tree
55
+ when String
56
+ yield tree
57
+ else
58
+ raise ArgumentError, 'Invalid type.'
59
+ end
60
+ end
61
+
62
+ # operate on substring
63
+ def replace_text!
64
+ map_substring_text_yield(@substrings) do |node|
65
+ case node
66
+ when Hash
67
+ text = yield node[@text_sym]
68
+ node[@text_sym] = text
69
+ when String
70
+ yield node
71
+ end
72
+ end
73
+ end
74
+
75
+ # Method to concatenate all substrings into a single string
76
+ def concatenate
77
+ concatenate_substrings(@substrings)
78
+ end
79
+
80
+ # Method to decorate all substrings into a single string
81
+ def decorate
82
+ decorate_substrings(@substrings)
83
+ end
84
+
85
+ # Handle string inspection methods and pass them to the concatenated string
86
+ def method_missing(method_name, *arguments, &block)
87
+ if ''.respond_to?(method_name)
88
+ concatenate.send(method_name, *arguments, &block)
89
+ else
90
+ super
91
+ end
92
+ end
93
+
94
+ # Ensure proper handling of method checks
95
+ def respond_to_missing?(method_name, include_private = false)
96
+ ''.respond_to?(method_name) || super
97
+ end
98
+
99
+ private
100
+
101
+ # Parse the input substrings into a nested array of hashes structure
102
+ def parse_substrings(substrings)
103
+ case substrings
104
+ when Hash
105
+ [substrings]
106
+ when Array
107
+ substrings.map { |s| parse_substrings(s) }
108
+ else
109
+ substrings
110
+ # raise ArgumentError, 'Invalid input type. Expected Hash or Array.'
111
+ end
112
+ end
113
+
114
+ # Recursively concatenate substrings
115
+ def concatenate_substrings(substrings)
116
+ substrings.map do |s|
117
+ case s
118
+ when Hash
119
+ s[@text_sym]
120
+ when Array
121
+ concatenate_substrings(s)
122
+ end
123
+ end.join
124
+ end
125
+
126
+ # Recursively decorate substrings
127
+ def decorate_substrings(substrings, prior_color = '')
128
+ substrings.map do |s|
129
+ case s
130
+ when Hash
131
+ if s[@style_sym]
132
+ AnsiString.new(s[@text_sym]).send(s[@style_sym]) + prior_color
133
+ else
134
+ s[@text_sym]
135
+ end
136
+ when Array
137
+ decorate_substrings(s, prior_color)
138
+ end
139
+ end.join
140
+ end
141
+ end
142
+
143
+ return if $PROGRAM_NAME != __FILE__
144
+
145
+ require 'minitest/autorun'
146
+
147
+ class TestHierarchyString < Minitest::Test
148
+ def setup
149
+ text_sym = :text
150
+ style_sym = :color
151
+
152
+ @single_hash = { text_sym => 'Hello', style_sym => :downcase }
153
+ @nested_hashes = [
154
+ { text_sym => 'Hello', style_sym => :downcase },
155
+ [
156
+ { text_sym => ' ', style_sym => nil },
157
+ { text_sym => 'World', style_sym => :upcase }
158
+ ]
159
+ ]
160
+ @hierarchy_single = HierarchyString.new(@single_hash)
161
+ @hierarchy_nested = HierarchyString.new(@nested_hashes)
162
+ end
163
+
164
+ def test_initialize_single_hash
165
+ text_sym = :text
166
+ style_sym = :color
167
+
168
+ assert_equal [{ text_sym => 'Hello', style_sym => :downcase }],
169
+ @hierarchy_single.substrings
170
+ end
171
+
172
+ def test_initialize_nested_hashes
173
+ text_sym = :text
174
+ style_sym = :color
175
+
176
+ expected = [
177
+ [{ text_sym => 'Hello', style_sym => :downcase }],
178
+ [
179
+ [{ text_sym => ' ', style_sym => nil }],
180
+ [{ text_sym => 'World', style_sym => :upcase }]
181
+ ]
182
+ ]
183
+ assert_equal expected, @hierarchy_nested.substrings
184
+ end
185
+
186
+ def test_concatenate_single_hash
187
+ assert_equal 'Hello', @hierarchy_single.concatenate
188
+ end
189
+
190
+ def test_concatenate_nested_hashes
191
+ assert_equal 'Hello World', @hierarchy_nested.concatenate
192
+ end
193
+
194
+ def test_decorate_single_hash
195
+ assert_equal 'Hello'.downcase, @hierarchy_single.decorate
196
+ end
197
+
198
+ def test_decorate_nested_hashes
199
+ assert_equal "#{'Hello'.downcase} #{'World'.upcase}",
200
+ @hierarchy_nested.decorate
201
+ end
202
+
203
+ def test_replace_text_single_hash
204
+ @hierarchy_single.replace_text!(&:upcase)
205
+ assert_equal 'HELLO', @hierarchy_single.concatenate
206
+ end
207
+
208
+ def test_replace_text_nested_hashes
209
+ @hierarchy_nested.replace_text!(&:upcase)
210
+ assert_equal 'HELLO WORLD', @hierarchy_nested.concatenate
211
+ end
212
+
213
+ def test_method_missing
214
+ assert_equal 'Hello', @hierarchy_single.capitalize
215
+ end
216
+
217
+ def test_respond_to_missing
218
+ assert @hierarchy_single.respond_to?(:capitalize)
219
+ refute @hierarchy_single.respond_to?(:non_existent_method)
220
+ end
221
+ end
@@ -47,11 +47,16 @@ class InputSequencer
47
47
 
48
48
  # Generates the next menu state based on provided attributes.
49
49
 
50
- def self.next_link_state(block_name: nil, display_menu: nil, document_filename: nil, prior_block_was_link: false)
50
+ def self.next_link_state(
51
+ block_name: nil, display_menu: nil, document_filename: nil,
52
+ inherited_lines: nil, keep_code: false, prior_block_was_link: false
53
+ )
51
54
  MarkdownExec::LinkState.new(
52
55
  block_name: block_name,
53
56
  display_menu: display_menu,
54
57
  document_filename: document_filename,
58
+ inherited_lines: inherited_lines,
59
+ keep_code: keep_code,
55
60
  prior_block_was_link: prior_block_was_link
56
61
  )
57
62
  end
@@ -59,6 +64,8 @@ class InputSequencer
59
64
  # Orchestrates the flow of menu states and user interactions.
60
65
  def run_yield(sym, *args, &block)
61
66
  block.call sym, *args
67
+ rescue AppInterrupt
68
+ raise
62
69
  # rubocop:disable Style/RescueStandardError
63
70
  rescue
64
71
  pp $!, $@
@@ -79,7 +86,8 @@ class InputSequencer
79
86
  )
80
87
  exit_when_bq_empty = !bq_is_empty? # true when running blocks from cli; unless "stay" is used
81
88
  loop do
82
- break if run_yield(:parse_document, now_menu.document_filename, &block) == :break
89
+ break if run_yield(:parse_document, now_menu.document_filename,
90
+ &block) == :break
83
91
 
84
92
  # self.imw_ins now_menu, 'now_menu'
85
93
 
@@ -90,16 +98,17 @@ class InputSequencer
90
98
  run_yield :display_menu, &block
91
99
 
92
100
  choice = run_yield :user_choice, &block
101
+ break if choice == :break
93
102
 
94
- raise 'Block not recognized.' if choice.nil?
95
- break if run_yield(:exit?, choice&.downcase, &block) # Exit loop and method to terminate the app
103
+ raise BlockMissing, 'Block not recognized.' if choice.nil?
104
+ # Exit loop and method to terminate the app
105
+ break if run_yield(:exit?, choice&.to_s.downcase, &block)
96
106
 
97
107
  next_state = run_yield :execute_block, choice, &block
98
108
  # imw_ins next_state, 'next_state'
99
- return :break if next_state == :break
109
+ break if next_state == :break
100
110
 
101
111
  next_menu = next_state
102
-
103
112
  else
104
113
  if now_menu.block_name && !now_menu.block_name.empty?
105
114
  block_name = now_menu.block_name
@@ -115,6 +124,8 @@ class InputSequencer
115
124
  InputSequencer.next_link_state(display_menu: true)
116
125
  else
117
126
  state = run_yield :execute_block, block_name, &block
127
+ break if state == :break
128
+
118
129
  state.display_menu = bq_is_empty?
119
130
  state
120
131
  end
@@ -123,11 +134,8 @@ class InputSequencer
123
134
  end
124
135
  now_menu = InputSequencer.merge_link_state(now_menu, next_menu)
125
136
  end
126
- # rubocop:disable Style/RescueStandardError
127
- rescue
128
- pp $!, $@
129
- exit 1
130
- # rubocop:enable Style/RescueStandardError
137
+
138
+ run_yield :close_ux, &block
131
139
  end
132
140
  end
133
141
 
@@ -68,7 +68,8 @@ module InstanceMethodWrapper
68
68
 
69
69
  def self.prepended(base)
70
70
  base.instance_methods(false).each do |method_name|
71
- wrap_method(base, method_name) unless %i[method_missing].include? method_name
71
+ wrap_method(base,
72
+ method_name) unless %i[method_missing].include? method_name
72
73
  end
73
74
 
74
75
  base.singleton_class.send(:define_method, :method_added) do |method_name|
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # encoding=utf-8
5
+
6
+ class LayeredHash
7
+ def initialize(table = {}, layers: %i[main])
8
+ @layers = layers.map { |layer| [layer, {}] }.to_h
9
+ @current_layer = layers.first
10
+ @layers[@current_layer] = table
11
+ end
12
+
13
+ private
14
+
15
+ def method_missing(method, *args, &block)
16
+ method_name = method.to_s
17
+ if @layers.respond_to?(method_name)
18
+ @layers.send(method_name, *args, &block)
19
+ elsif method_name[-1] == '='
20
+ @layers[method_name.chop.to_sym] = args[0]
21
+ elsif @layers.respond_to?(method_name)
22
+ @layers.send(method_name, *args)
23
+ else
24
+ @layers[method_name.to_sym]
25
+ end
26
+ rescue StandardError => err
27
+ warn("ERROR ** LayeredHash.method_missing(method: #{method_name}," \
28
+ " *args: #{args.inspect}, &block)")
29
+ warn err.inspect
30
+ warn(caller[0..4])
31
+ raise err
32
+ end
33
+
34
+ public
35
+
36
+ # Retrieves the value of a key from the current layer using hash notation
37
+ def [](key)
38
+ raise "Current layer not set" unless @current_layer
39
+
40
+ get_from_layer(@current_layer, key)
41
+ end
42
+
43
+ # Sets a key-value pair in the current layer using hash notation
44
+ def []=(key, value)
45
+ raise "Current layer not set" unless @current_layer
46
+
47
+ set(@current_layer, key, value)
48
+ end
49
+
50
+ def fetch(key, *args)
51
+ key_sym = key.to_sym
52
+ if respond_to?("get_#{key}")
53
+ send("get_#{key}")
54
+ # elsif @table.key?(key_sym)
55
+ elsif @layers[@current_layer].key?(key_sym)
56
+ # @table[key_sym]
57
+ @layers[@current_layer][key_sym]
58
+ elsif block_given?
59
+ yield key_sym
60
+ elsif args.count.positive?
61
+ args.first
62
+ else
63
+ binding.irb
64
+ raise KeyError, "key not found: #{key}"
65
+ end.tap { |ret|
66
+ pp([__LINE__, "Poly.fetch #{key} #{args}", '->',
67
+ ret]) if $pd
68
+ }
69
+ end
70
+
71
+ # Retrieves the value of a key from the highest priority layer that has a value
72
+ def get(key)
73
+ @layers.reverse_each do |_, hash|
74
+ return hash[key] if hash.key?(key)
75
+ end
76
+ nil
77
+ end
78
+
79
+ # Retrieves the value of a key from the specified layer
80
+ def get_from_layer(layer, key)
81
+ if @layers.key?(layer)
82
+ @layers[layer][key]
83
+ else
84
+ raise ArgumentError, "Layer #{layer} does not exist"
85
+ end
86
+ end
87
+
88
+ def merge(*args)
89
+ @layers.merge(*args).tap { |ret|
90
+ pp([__LINE__, "LayeredHash.merge", '->', ret]) if $pd
91
+ }
92
+ end
93
+
94
+ def respond_to_missing?(method_name, include_private = false)
95
+ @layers.key?(method_name.to_sym) || super
96
+ end
97
+
98
+ # Sets a key-value pair in the specified layer
99
+ def set(layer, key, value)
100
+ if @layers.key?(layer)
101
+ @layers[layer][key] = value
102
+ else
103
+ raise ArgumentError, "Layer #{layer} does not exist"
104
+ end
105
+ end
106
+
107
+ # Sets the current layer for use with hash notation ([])
108
+ def set_current_layer(layer)
109
+ if @layers.key?(layer)
110
+ @current_layer = layer
111
+ else
112
+ raise ArgumentError, "Layer #{layer} does not exist"
113
+ end
114
+ end
115
+
116
+ def to_h
117
+ @layers.to_h
118
+ end
119
+ end
120
+
121
+ return if $PROGRAM_NAME != __FILE__
122
+
123
+ layered_hash = LayeredHash.new(layers: %i[low high])
124
+
125
+ # Set current layer
126
+ layered_hash.set_current_layer(:low)
127
+
128
+ # Set values in the current layer using hash notation
129
+ layered_hash[:key1] = 'low_value'
130
+ layered_hash[:key2] = 'low_only_value'
131
+
132
+ # Switch current layer
133
+ layered_hash.set_current_layer(:high)
134
+
135
+ # Set values in the new current layer using hash notation
136
+ layered_hash[:key1] = 'high_value'
137
+
138
+ # Get value from the specific current layer using hash notation
139
+ puts layered_hash[:key1] # Output: 'high_value'
140
+
141
+ # Get value from the highest priority layer
142
+ puts layered_hash.get(:key1) # Output: 'high_value'
143
+ puts layered_hash.get(:key2) # Output: 'low_only_value'
data/lib/link_history.rb CHANGED
@@ -9,7 +9,7 @@ module MarkdownExec
9
9
  class LinkState
10
10
  attr_accessor :block_name, :display_menu, :document_filename,
11
11
  :inherited_block_names, :inherited_dependencies,
12
- :prior_block_was_link
12
+ :keep_code, :prior_block_was_link
13
13
 
14
14
  # Initialize the LinkState with keyword arguments for each attribute.
15
15
  # @param block_name [String, nil] the name of the block.
@@ -19,13 +19,14 @@ module MarkdownExec
19
19
  # @param inherited_lines [Array<String>, nil] the inherited lines of code.
20
20
  def initialize(block_name: nil, display_menu: nil, document_filename: nil,
21
21
  inherited_block_names: [], inherited_dependencies: nil, inherited_lines: nil,
22
- prior_block_was_link: nil)
22
+ keep_code: false, prior_block_was_link: nil)
23
23
  @block_name = block_name
24
24
  @display_menu = display_menu
25
25
  @document_filename = document_filename
26
26
  @inherited_block_names = inherited_block_names
27
27
  @inherited_dependencies = inherited_dependencies
28
28
  @inherited_lines = inherited_lines
29
+ @keep_code = keep_code
29
30
  @prior_block_was_link = prior_block_was_link
30
31
  end
31
32
 
@@ -46,27 +47,38 @@ module MarkdownExec
46
47
  other.inherited_block_names == inherited_block_names &&
47
48
  other.inherited_dependencies == inherited_dependencies &&
48
49
  other.inherited_lines == inherited_lines &&
50
+ other.keep_code == keep_code &&
49
51
  other.prior_block_was_link == prior_block_was_link
50
52
  end
51
53
 
52
54
  def inherited_lines
53
- @inherited_lines.tap { |ret| pp ['LinkState.inherited_lines() ->', ret] if $pd }
55
+ @inherited_lines.tap { |ret|
56
+ pp ['LinkState.inherited_lines() ->', ret] if $pd
57
+ }
54
58
  end
55
59
 
56
60
  def inherited_lines=(value)
57
- @inherited_lines = value.tap { |ret| pp ['LinkState.inherited_lines=() ->', ret] if $pd }
61
+ @inherited_lines = value.tap { |ret|
62
+ pp ['LinkState.inherited_lines=() ->', ret] if $pd
63
+ }
58
64
  end
59
65
 
60
66
  def inherited_lines_append(value)
61
- @inherited_lines = ((@inherited_lines || []) + value).tap { |ret| pp ['LinkState.inherited_lines_append() ->', ret] if $pd }
67
+ @inherited_lines = ((@inherited_lines || []) + value).tap { |ret|
68
+ pp ['LinkState.inherited_lines_append() ->', ret] if $pd
69
+ }
62
70
  end
63
71
 
64
72
  def inherited_lines_block
65
- @inherited_lines.join("\n").tap { |ret| pp ['LinkState.inherited_lines_block() ->', ret] if $pd }
73
+ (@inherited_lines || []).join("\n").tap { |ret|
74
+ pp ['LinkState.inherited_lines_block() ->', ret] if $pd
75
+ }
66
76
  end
67
77
 
68
78
  def inherited_lines_count
69
- (@inherited_lines&.count || 0).tap { |ret| pp ['LinkState.inherited_lines_count() ->', ret] if $pd }
79
+ (@inherited_lines&.count || 0).tap { |ret|
80
+ pp ['LinkState.inherited_lines_count() ->', ret] if $pd
81
+ }
70
82
  end
71
83
 
72
84
  def inherited_lines_map
@@ -76,7 +88,9 @@ module MarkdownExec
76
88
  end
77
89
 
78
90
  def inherited_lines_present?
79
- @inherited_lines.present?.tap { |ret| pp ['LinkState.inherited_lines_present?() ->', ret] if $pd }
91
+ @inherited_lines.present?.tap { |ret|
92
+ pp ['LinkState.inherited_lines_present?() ->', ret] if $pd
93
+ }
80
94
  end
81
95
  end
82
96
 
@@ -7,5 +7,5 @@ module MarkdownExec
7
7
  BIN_NAME = 'mde'
8
8
  GEM_NAME = 'markdown_exec'
9
9
  TAP_DEBUG = 'MDE_DEBUG'
10
- VERSION = '2.2.0'
10
+ VERSION = '2.4.0'
11
11
  end