markdown_exec 3.2.0 → 3.3.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 +27 -0
  3. data/Gemfile.lock +1 -1
  4. data/Rakefile +3 -3
  5. data/bats/block-type-ux-auto.bats +1 -1
  6. data/bats/block-type-ux-default.bats +1 -1
  7. data/bats/block-type-ux-echo-hash-transform.bats +1 -1
  8. data/bats/block-type-ux-echo-hash.bats +2 -2
  9. data/bats/block-type-ux-exec-hash-transform.bats +8 -0
  10. data/bats/block-type-ux-exec-hash.bats +15 -0
  11. data/bats/block-type-ux-exec.bats +1 -1
  12. data/bats/block-type-ux-force.bats +9 -0
  13. data/bats/block-type-ux-formats.bats +8 -0
  14. data/bats/block-type-ux-readonly.bats +1 -1
  15. data/bats/block-type-ux-row-format.bats +1 -1
  16. data/bats/block-type-ux-transform.bats +1 -1
  17. data/bats/import-directive-parameter-symbols.bats +9 -0
  18. data/bats/import-duplicates.bats +4 -2
  19. data/bats/import-parameter-symbols.bats +8 -0
  20. data/bats/markup.bats +1 -1
  21. data/bats/options.bats +1 -1
  22. data/bin/tab_completion.sh +5 -1
  23. data/docs/dev/block-type-ux-echo-hash-transform.md +14 -12
  24. data/docs/dev/block-type-ux-exec-hash-transform.md +37 -0
  25. data/docs/dev/block-type-ux-exec-hash.md +93 -0
  26. data/docs/dev/block-type-ux-force.md +20 -0
  27. data/docs/dev/block-type-ux-formats.md +58 -0
  28. data/docs/dev/hexdump_format.md +267 -0
  29. data/docs/dev/import/parameter-symbols.md +6 -0
  30. data/docs/dev/import-directive-parameter-symbols.md +9 -0
  31. data/docs/dev/import-parameter-symbols-template.md +24 -0
  32. data/docs/dev/import-parameter-symbols.md +6 -0
  33. data/docs/dev/load-vars-state-demo.md +35 -0
  34. data/docs/ux-blocks-examples.md +2 -3
  35. data/examples/import_with_substitution_demo.md +130 -26
  36. data/examples/imports/organism_template.md +86 -29
  37. data/lib/cached_nested_file_reader.rb +265 -27
  38. data/lib/constants.rb +8 -1
  39. data/lib/env_interface.rb +13 -7
  40. data/lib/evaluate_shell_expressions.rb +1 -0
  41. data/lib/fcb.rb +120 -28
  42. data/lib/format_table.rb +56 -23
  43. data/lib/fout.rb +5 -0
  44. data/lib/hash_delegator.rb +1158 -347
  45. data/lib/markdown_exec/version.rb +1 -1
  46. data/lib/markdown_exec.rb +2 -0
  47. data/lib/mdoc.rb +13 -11
  48. data/lib/menu.src.yml +139 -34
  49. data/lib/menu.yml +116 -32
  50. data/lib/string_util.rb +80 -0
  51. data/lib/table_extractor.rb +170 -64
  52. data/lib/ww.rb +325 -29
  53. metadata +18 -2
data/lib/ww.rb CHANGED
@@ -5,65 +5,156 @@ require 'bundler/setup' # Bundler enforces gem versions
5
5
  require 'pp'
6
6
  require 'stringio'
7
7
 
8
+ # call depth icons
9
+ DEPTH_ICON = '›'
10
+
11
+ # log levels
8
12
  LOG_LEVELS = %i[debug info warn error fatal].freeze
9
13
 
10
- $debug = $DEBUG || !ENV['WW'].nil?
14
+ # is enabled
15
+ def enable_debugging
16
+ ENV.fetch('WW', '0').to_i.positive?
17
+ end
18
+
19
+ # is enabled, not silent
20
+ def env_show_attribution
21
+ ENV['WW'] != '0'
22
+ end
23
+
24
+ def is_new_alg?
25
+ # use the new algo only if env var is ALG is not empty
26
+ !ENV.fetch('ALG', '').empty?
27
+
28
+ # use the new algo if ALG != 0
29
+ # ENV.fetch('ALG', '') != '0'
30
+ end
31
+
32
+ # enable application-wide debugging
33
+ $debug = $DEBUG || enable_debugging
34
+
35
+ # no default category
36
+ $ww_category = nil
37
+
38
+ # no default log file
39
+ $ww_log_file = nil
40
+
41
+ # default output to $stderr
42
+ $ww_output = $stderr
43
+ unless ($id = ENV.fetch('WW_LOG', '')).empty?
44
+ alg = is_new_alg? ? '1' : '0'
45
+ # local log file with timestamp and algo name
46
+ $ww_log_file = "#{Time.now.utc.strftime '%H-%M-%S'}-#{$id}-#{alg}.log"
47
+ end
11
48
 
12
49
  # attribution in output unless disabled
13
- if $debug && ENV['WW_MINIMUM'].nil?
14
- warn "WW Debugging per $DEBUG ('ruby --debug')" if $DEBUG
15
- warn 'WW Debugging per environment variable WW' unless ENV['WW'].nil?
50
+ if env_show_attribution
51
+ if $debug
52
+ # not silent, display notice
53
+ if $DEBUG
54
+ # debugging triggered by Ruby debug
55
+ warn "WW Debugging per $DEBUG ('ruby --debug')"
56
+ else
57
+ # debugging triggered by WW environment variable
58
+ warn 'WW Debugging per environment variable WW'
59
+ end
60
+ end
61
+ if is_new_alg?
62
+ warn "WW Testing a new algorithm. Control with env var 'ALG'"
63
+ end
16
64
  end
17
65
 
66
+ # selectively enabled, for general debugging
67
+ # return the last item in the list
18
68
  def ww(*objs, **kwargs)
19
- # return the last item in the list, as the label is usually first
69
+ # assume the final item is the significant one
70
+ # allows prefixing to an existing expression and forwarding the result
20
71
  return objs.last unless $debug
21
72
 
22
73
  locations = kwargs[:locations] || caller_locations
23
74
  ww0(*objs, **kwargs.merge(locations: locations))
24
75
  end
25
76
 
26
- # select enabled, for exceptions
27
- # print a data object for the error, and the failing line
77
+ # output the object and backtrace for the error
78
+ # abort
79
+ def wwa(*objs, **kwargs)
80
+ ww0(*objs,
81
+ **kwargs.merge(full_backtrace: true,
82
+ locations: caller_locations))
83
+
84
+ exit 1
85
+ end
86
+
87
+ # output the object and backtrace for the error
88
+ # raise the error for the caller to handle
28
89
  def wwe(*objs, **kwargs)
29
90
  ww0(*objs,
30
91
  **kwargs.merge(full_backtrace: true,
31
92
  locations: caller_locations))
32
93
 
33
- raise StandardError, objs.first[:error]
94
+ # raise StandardError, objs.first.fetch(:error) || objs.first
95
+ raise StandardError, objs.first
34
96
  end
35
97
 
36
98
  # selectively enabled, for process tracking
37
- # print the failing line
99
+ # output data and the caller's location
38
100
  def wwp(*objs, **kwargs)
39
101
  return objs.last unless $debug
40
102
 
41
- ww(*objs, **kwargs.merge(locations: caller_locations[0..0]))
103
+ ww0(*objs,
104
+ **kwargs.merge(
105
+ locations: caller_locations[0..0],
106
+ location_offset: caller_locations.count
107
+ ))
108
+ end
109
+
110
+ # the return value for a function
111
+ def wwr(*objs, **kwargs)
112
+ # assume the final item is the significant one
113
+ # allows prefixing to an existing expression and forwarding the result
114
+ return objs.last unless $debug
115
+
116
+ ww0(*objs,
117
+ **kwargs.merge(
118
+ locations: caller_locations[0..0],
119
+ location_offset: caller_locations.count
120
+ ))
42
121
  end
43
122
 
44
- # selectively enabled, for tagged
45
- # print the failing line
46
- # eg wwt :line, 'data:', data
123
+ # selectively enabled, for tagged data
124
+ # the first item is the tag, the rest is data
125
+ # exclude tags in the list of tags to skip
126
+ # output data and the caller's location
47
127
  def wwt(*objs, **kwargs)
48
- return objs.last if !$debug || %i[env].include?(objs.first)
128
+ # tags to skip
129
+ return objs.last if !$debug || %i[blocks env fcb].include?(objs.first)
49
130
 
50
131
  formatted = ['Tagged', objs.first] + objs[1..]
51
- ww(*formatted, **kwargs.merge(locations: caller_locations[0..0]))
132
+ ww0(*formatted,
133
+ **kwargs.merge(
134
+ locations: caller_locations[0..0],
135
+ location_offset: caller_locations.count
136
+ ))
52
137
  end
53
138
 
139
+ # output the formatted data and location
54
140
  def ww0(*objs,
55
- category: nil,
141
+ category: $ww_category,
56
142
  full_backtrace: false,
57
143
  level: :debug,
58
144
  locations: caller_locations,
59
- log_file: nil,
60
- output: $stderr,
145
+ log_file: $ww_log_file,
146
+ output: $ww_output,
61
147
  single_line: false,
62
- timestamp: false)
148
+ timestamp: false,
149
+ location_offset: 0)
63
150
  # Format caller information line
64
- def caller_info_line(caller_info)
65
- "#{caller_info.lineno} : #{caller_info.path.sub(%r{^#{Dir.pwd}},
66
- '')} : #{caller_info.label}"
151
+ caller_info_line = lambda do |caller_info, ind|
152
+ [
153
+ DEPTH_ICON * (location_offset + locations.count - ind),
154
+ caller_info.path.deref,
155
+ caller_info.lineno,
156
+ caller_info.label
157
+ ].join(' : ')
67
158
  end
68
159
  # Validate log level
69
160
  raise ArgumentError,
@@ -71,9 +162,11 @@ def ww0(*objs,
71
162
 
72
163
  # Generate backtrace
73
164
  backtrace = if full_backtrace
74
- locations.map { |caller_info| caller_info_line(caller_info) }
165
+ locations.map.with_index do |caller_info, ind|
166
+ caller_info_line.call(caller_info, ind)
167
+ end
75
168
  else
76
- [caller_info_line(locations.first)]
169
+ [caller_info_line.call(locations.first, 0)]
77
170
  end
78
171
 
79
172
  # Add optional timestamp
@@ -95,8 +188,12 @@ def ww0(*objs,
95
188
  "#{header}\n#{io.string}"
96
189
  end
97
190
 
191
+ # prefix each line in formatted_message
192
+ prefix = (' ' * 8).freeze
193
+ formatted_message = prefix + formatted_message.gsub("\n", "\n#{prefix}")
194
+
98
195
  # Output to $stderr or specified IO object
99
- output.puts formatted_message
196
+ output.puts "\033[38;2;128;191;191m#{formatted_message}\033[0m"
100
197
  output.flush
101
198
 
102
199
  # Optionally log to a file
@@ -106,16 +203,22 @@ def ww0(*objs,
106
203
  file.puts(formatted_message)
107
204
  end
108
205
 
109
- def wwb
110
- binding.irb if $debug
111
- end
112
-
113
206
  # return the last item in the list, as the label is usually first
114
207
  objs.last
115
208
  end
116
209
 
210
+ # break into the debugger if enabled
211
+ def wwb
212
+ binding.irb if $debug
213
+ end
214
+
117
215
  class Array
118
216
  unless defined?(deref)
217
+
218
+ # trim the backtrace to project source files
219
+ # exclude vendor and .bundle directories
220
+ # limit the count to 4
221
+ # replace the home directory with a .
119
222
  def deref(count = 4)
120
223
  dir_pwd = Dir.pwd
121
224
  map(&:deref).reject do |line|
@@ -129,8 +232,201 @@ end
129
232
 
130
233
  class String
131
234
  unless defined?(deref)
235
+ # replace the app's directory with a .
132
236
  def deref
133
237
  sub(%r{^#{Dir.pwd}}, '')
134
238
  end
135
239
  end
136
240
  end
241
+
242
+ class Array
243
+ # Count occurrences by method result using tally (Ruby 2.7+)
244
+ #
245
+ # @param method [Symbol, String] The method to call on each element
246
+ # @return [Hash] Hash of method result => count pairs
247
+ def count_by(method)
248
+ raise ArgumentError,
249
+ 'Method must be a Symbol or String' unless method.is_a?(Symbol) || method.is_a?(String)
250
+
251
+ if respond_to?(:tally) # Ruby 2.7+
252
+ map do |item|
253
+ item.respond_to?(method) ? item.send(method) : nil
254
+ end.compact.tally
255
+ else
256
+ # Fallback for older Ruby versions
257
+ result = Hash.new(0)
258
+ each do |item|
259
+ if item.respond_to?(method)
260
+ value = item.send(method)
261
+ result[value] += 1
262
+ end
263
+ end
264
+ result
265
+ end
266
+ rescue NoMethodError => err
267
+ warn "Method #{method} not available on some items: #{err.message}"
268
+ {}
269
+ end
270
+
271
+ # Use filter_map for combined filtering and mapping (Ruby 2.7+)
272
+ # Filters elements and transforms them in one pass
273
+ #
274
+ # @param method [Symbol, String] The method to call on each element
275
+ # @param value [Object] The value to match against
276
+ # @param transform_method [Symbol, String, nil] Optional
277
+ # method to call on matching elements
278
+ # @return [Array] Array of transformed matching elements
279
+ def filter_map_by(method, value, transform_method = nil)
280
+ raise ArgumentError,
281
+ 'Method must be a Symbol or String' unless method.is_a?(Symbol) || method.is_a?(String)
282
+
283
+ if respond_to?(:filter_map) # Ruby 2.7+
284
+ filter_map do |item|
285
+ if item.respond_to?(method) && item.send(method) == value
286
+ transform_method ? item.send(transform_method) : item
287
+ end
288
+ end
289
+ else
290
+ # Fallback for older Ruby versions
291
+ result = []
292
+ each do |item|
293
+ if item.respond_to?(method) && item.send(method) == value
294
+ result << (transform_method ? item.send(transform_method) : item)
295
+ end
296
+ end
297
+ result
298
+ end
299
+ rescue NoMethodError => err
300
+ warn "Method #{method} not available on some items: #{err.message}"
301
+ []
302
+ end
303
+
304
+ # Find the first element where the specified method matches a value
305
+ # Supports both method-based and block-based filtering
306
+ #
307
+ # @param method [Symbol, String, nil] The method to call on each element
308
+ # @param value [Object, nil] The value to match against
309
+ # @param default [Object, nil] Default value if no match found
310
+ # @param block [Proc, nil] Optional block for custom filtering logic
311
+ # @return [Object, nil] The first matching element or default
312
+ def find_by(method = nil, value = nil, default = nil, &block)
313
+ if block_given?
314
+ find(&block)
315
+ else
316
+ raise ArgumentError,
317
+ 'Method must be a Symbol or String' unless method.is_a?(Symbol) || method.is_a?(String)
318
+
319
+ find do |item|
320
+ item.respond_to?(method) && item.send(method) == value
321
+ end || default
322
+ end
323
+ rescue NoMethodError => err
324
+ warn "Method #{method} not available on some items: #{err.message}"
325
+ default
326
+ end
327
+
328
+ # Find elements using hash-based conditions
329
+ # All conditions must match for an element to be included
330
+ #
331
+ # @param conditions [Hash] Hash of method => value pairs
332
+ # @return [Array] Array of matching elements
333
+ def find_where(conditions = {})
334
+ find do |item|
335
+ conditions.all? do |method, value|
336
+ item.respond_to?(method) && item.send(method) == value
337
+ end
338
+ end
339
+ rescue NoMethodError => err
340
+ warn "Some methods not available on items: #{err.message}"
341
+ nil
342
+ end
343
+
344
+ # Match elements using pattern matching (Ruby 2.7+)
345
+ # Uses grep for pattern matching against method results
346
+ #
347
+ # @param method [Symbol, String] The method to call on each element
348
+ # @param pattern [Regexp, Object] Pattern to match against
349
+ # @return [Array] Array of matching elements
350
+ def match_by(method, pattern)
351
+ raise ArgumentError,
352
+ 'Method must be a Symbol or String' unless method.is_a?(Symbol) || method.is_a?(String)
353
+
354
+ grep { |item| pattern === item.send(method) }
355
+ rescue NoMethodError => err
356
+ warn "Method #{method} not available on some items: #{err.message}"
357
+ []
358
+ end
359
+
360
+ # Partition elements based on method result
361
+ #
362
+ # @param method [Symbol, String] The method to call on each element
363
+ # @param value [Object] The value to partition by
364
+ # @return [Array] Array containing [matching_elements, non_matching_elements]
365
+ def partition_by(method, value)
366
+ raise ArgumentError,
367
+ 'Method must be a Symbol or String' unless method.is_a?(Symbol) || method.is_a?(String)
368
+
369
+ partition { |item| item.respond_to?(method) && item.send(method) == value }
370
+ rescue NoMethodError => err
371
+ warn "Method #{method} not available on some items: #{err.message}"
372
+ [[], self]
373
+ end
374
+
375
+ # Reject elements where the specified method matches a value
376
+ # Supports both method-based and block-based filtering
377
+ #
378
+ # @param method [Symbol, String, nil] The method to call on each element
379
+ # @param value [Object, nil] The value to match against
380
+ # @param block [Proc, nil] Optional block for custom filtering logic
381
+ # @return [Array] Array of non-matching elements
382
+ def reject_by(method = nil, value = nil, &block)
383
+ if block_given?
384
+ reject(&block)
385
+ else
386
+ raise ArgumentError,
387
+ 'Method must be a Symbol or String' unless method.is_a?(Symbol) || method.is_a?(String)
388
+
389
+ reject { |item| item.respond_to?(method) && item.send(method) == value }
390
+ end
391
+ rescue NoMethodError => err
392
+ warn "Method #{method} not available on some items: #{err.message}"
393
+ self
394
+ end
395
+
396
+ # Select elements where the specified method matches a value
397
+ # Supports both method-based and block-based filtering
398
+ #
399
+ # @param method [Symbol, String, nil] The method to call on each element
400
+ # @param value [Object, nil] The value to match against
401
+ # @param block [Proc, nil] Optional block for custom filtering logic
402
+ # @return [Array] Array of matching elements
403
+ def select_by(method = nil, value = nil, &block)
404
+ if block_given?
405
+ select(&block)
406
+ else
407
+ raise ArgumentError,
408
+ 'Method must be a Symbol or String' unless method.is_a?(Symbol) || method.is_a?(String)
409
+
410
+ select { |item| item.respond_to?(method) && item.send(method) == value }
411
+ end
412
+ rescue NoMethodError => err
413
+ warn "Method #{method} not available on some items: #{err.message}"
414
+ []
415
+ end
416
+
417
+ # Select elements using hash-based conditions
418
+ # All conditions must match for an element to be included
419
+ #
420
+ # @param conditions [Hash] Hash of method => value pairs
421
+ # @return [Array] Array of matching elements
422
+ def select_where(conditions = {})
423
+ select do |item|
424
+ conditions.all? do |method, value|
425
+ item.respond_to?(method) && item.send(method) == value
426
+ end
427
+ end
428
+ rescue NoMethodError => err
429
+ warn "Some methods not available on items: #{err.message}"
430
+ []
431
+ end
432
+ end
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: 3.2.0
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fareed Stevenson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-24 00:00:00.000000000 Z
11
+ date: 2025-09-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: clipboard
@@ -120,7 +120,11 @@ files:
120
120
  - bats/block-type-ux-echo-hash-transform.bats
121
121
  - bats/block-type-ux-echo-hash.bats
122
122
  - bats/block-type-ux-echo.bats
123
+ - bats/block-type-ux-exec-hash-transform.bats
124
+ - bats/block-type-ux-exec-hash.bats
123
125
  - bats/block-type-ux-exec.bats
126
+ - bats/block-type-ux-force.bats
127
+ - bats/block-type-ux-formats.bats
124
128
  - bats/block-type-ux-hidden.bats
125
129
  - bats/block-type-ux-invalid.bats
126
130
  - bats/block-type-ux-readonly.bats
@@ -139,7 +143,9 @@ files:
139
143
  - bats/fail.bats
140
144
  - bats/history.bats
141
145
  - bats/import-conflict.bats
146
+ - bats/import-directive-parameter-symbols.bats
142
147
  - bats/import-duplicates.bats
148
+ - bats/import-parameter-symbols.bats
143
149
  - bats/import-with-text-substitution.bats
144
150
  - bats/import.bats
145
151
  - bats/indented-block-type-vars.bats
@@ -179,7 +185,11 @@ files:
179
185
  - docs/dev/block-type-ux-echo-hash-transform.md
180
186
  - docs/dev/block-type-ux-echo-hash.md
181
187
  - docs/dev/block-type-ux-echo.md
188
+ - docs/dev/block-type-ux-exec-hash-transform.md
189
+ - docs/dev/block-type-ux-exec-hash.md
182
190
  - docs/dev/block-type-ux-exec.md
191
+ - docs/dev/block-type-ux-force.md
192
+ - docs/dev/block-type-ux-formats.md
183
193
  - docs/dev/block-type-ux-hidden.md
184
194
  - docs/dev/block-type-ux-invalid.md
185
195
  - docs/dev/block-type-ux-readonly.md
@@ -196,11 +206,15 @@ files:
196
206
  - docs/dev/data-blocks.md
197
207
  - docs/dev/disable.md
198
208
  - docs/dev/document-shell.md
209
+ - docs/dev/hexdump_format.md
199
210
  - docs/dev/import-conflict-0.md
200
211
  - docs/dev/import-conflict-1.md
212
+ - docs/dev/import-directive-parameter-symbols.md
201
213
  - docs/dev/import-duplicates-0.md
202
214
  - docs/dev/import-duplicates-1.md
203
215
  - docs/dev/import-missing.md
216
+ - docs/dev/import-parameter-symbols-template.md
217
+ - docs/dev/import-parameter-symbols.md
204
218
  - docs/dev/import-substitution-basic.md
205
219
  - docs/dev/import-substitution-compare.md
206
220
  - docs/dev/import-substitution-export.md
@@ -214,12 +228,14 @@ files:
214
228
  - docs/dev/import-substitution-taxonomy.md
215
229
  - docs/dev/import-with-text-substitution.md
216
230
  - docs/dev/import.md
231
+ - docs/dev/import/parameter-symbols.md
217
232
  - docs/dev/indented-block-type-vars.md
218
233
  - docs/dev/indented-multi-line-output.md
219
234
  - docs/dev/line-decor-dynamic.md
220
235
  - docs/dev/line-wrapping.md
221
236
  - docs/dev/linked-file.md
222
237
  - docs/dev/load-mode-demo.md
238
+ - docs/dev/load-vars-state-demo.md
223
239
  - docs/dev/load1.sh
224
240
  - docs/dev/load_code.md
225
241
  - docs/dev/manage-saved-documents.md