markdown_exec 2.3.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +11 -2
  3. data/CHANGELOG.md +19 -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 +2 -2
  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 +39 -11
  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 +888 -603
  50. data/lib/hierarchy_string.rb +113 -25
  51. data/lib/input_sequencer.rb +16 -10
  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 +413 -165
  57. data/lib/mdoc.rb +27 -34
  58. data/lib/menu.src.yml +825 -710
  59. data/lib/menu.yml +799 -703
  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 +144 -8
  71. metadata +26 -3
  72. data/lib/std_out_err_logger.rb +0 -119
@@ -1,12 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'ansi_string'
4
+
3
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.
4
29
  class HierarchyString
5
30
  attr_accessor :substrings
6
31
 
7
32
  # Initialize with a single hash or an array of hashes
8
- def initialize(substrings)
33
+ def initialize(substrings, text_sym: :text, style_sym: :color)
9
34
  @substrings = parse_substrings(substrings)
35
+ @text_sym = text_sym
36
+ @style_sym = style_sym
10
37
  end
11
38
 
12
39
  def map_substring_text_yield(tree, &block)
@@ -21,8 +48,8 @@ class HierarchyString
21
48
  end
22
49
  end
23
50
  when Hash
24
- text = yield tree[:text]
25
- tree[:text] = text
51
+ text = yield tree[@text_sym]
52
+ tree[@text_sym] = text
26
53
 
27
54
  tree
28
55
  when String
@@ -37,8 +64,8 @@ class HierarchyString
37
64
  map_substring_text_yield(@substrings) do |node|
38
65
  case node
39
66
  when Hash
40
- text = yield node[:text]
41
- node[:text] = text
67
+ text = yield node[@text_sym]
68
+ node[@text_sym] = text
42
69
  when String
43
70
  yield node
44
71
  end
@@ -89,7 +116,7 @@ class HierarchyString
89
116
  substrings.map do |s|
90
117
  case s
91
118
  when Hash
92
- s[:text]
119
+ s[@text_sym]
93
120
  when Array
94
121
  concatenate_substrings(s)
95
122
  end
@@ -101,10 +128,10 @@ class HierarchyString
101
128
  substrings.map do |s|
102
129
  case s
103
130
  when Hash
104
- if s[:color]
105
- s[:text].send(s[:color]) + prior_color
131
+ if s[@style_sym]
132
+ AnsiString.new(s[@text_sym]).send(s[@style_sym]) + prior_color
106
133
  else
107
- s[:text]
134
+ s[@text_sym]
108
135
  end
109
136
  when Array
110
137
  decorate_substrings(s, prior_color)
@@ -115,19 +142,80 @@ end
115
142
 
116
143
  return if $PROGRAM_NAME != __FILE__
117
144
 
118
- # require 'bundler/setup'
119
- # Bundler.require(:default)
120
-
121
- # require 'fcb'
122
- # require 'minitest/autorun'
123
-
124
- # Usage
125
- hierarchy = HierarchyString.new([{ text: 'Hello ', color: :red },
126
- [{ text: 'World', color: :upcase },
127
- { text: '!' }]])
128
- puts hierarchy.decorate
129
- puts hierarchy.length
130
- # puts hierarchy.concatenate # Outputs: Hello World!
131
- # puts hierarchy.upcase # Outputs: HELLO WORLD!
132
- # puts hierarchy.length # Outputs: 12
133
- # puts hierarchy.gsub('World', 'Ruby') # Outputs: Hello Ruby!
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 $!, $@
@@ -91,17 +98,17 @@ class InputSequencer
91
98
  run_yield :display_menu, &block
92
99
 
93
100
  choice = run_yield :user_choice, &block
101
+ break if choice == :break
94
102
 
95
- raise 'Block not recognized.' if choice.nil?
103
+ raise BlockMissing, 'Block not recognized.' if choice.nil?
96
104
  # Exit loop and method to terminate the app
97
- break if run_yield(:exit?, choice&.downcase, &block)
105
+ break if run_yield(:exit?, choice&.to_s.downcase, &block)
98
106
 
99
107
  next_state = run_yield :execute_block, choice, &block
100
108
  # imw_ins next_state, 'next_state'
101
- return :break if next_state == :break
109
+ break if next_state == :break
102
110
 
103
111
  next_menu = next_state
104
-
105
112
  else
106
113
  if now_menu.block_name && !now_menu.block_name.empty?
107
114
  block_name = now_menu.block_name
@@ -117,6 +124,8 @@ class InputSequencer
117
124
  InputSequencer.next_link_state(display_menu: true)
118
125
  else
119
126
  state = run_yield :execute_block, block_name, &block
127
+ break if state == :break
128
+
120
129
  state.display_menu = bq_is_empty?
121
130
  state
122
131
  end
@@ -125,11 +134,8 @@ class InputSequencer
125
134
  end
126
135
  now_menu = InputSequencer.merge_link_state(now_menu, next_menu)
127
136
  end
128
- # rubocop:disable Style/RescueStandardError
129
- rescue
130
- pp $!, $@
131
- exit 1
132
- # rubocop:enable Style/RescueStandardError
137
+
138
+ run_yield :close_ux, &block
133
139
  end
134
140
  end
135
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.3.0'
10
+ VERSION = '2.4.0'
11
11
  end