markdown_exec 2.3.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 +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