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
@@ -0,0 +1,406 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
4
+
5
+ require_relative 'hierarchy_string'
6
+
7
+ module MarkdownTableFormatter
8
+ module_function
9
+
10
+ def format_table(lines, columns, decorate: nil)
11
+ rows = parse_rows(lines, columns)
12
+
13
+ alignment_indicators, column_widths =
14
+ calculate_column_alignment_and_widths(rows, columns)
15
+
16
+ format_rows(rows, alignment_indicators, column_widths, decorate)
17
+ end
18
+
19
+ def parse_rows(lines, columns)
20
+ role = :header_row
21
+ counter = -1
22
+
23
+ lines.map.with_index do |line, _row_ind|
24
+ line += '|' unless line.end_with?('|')
25
+ counter += 1
26
+
27
+ role = update_role(role, line)
28
+ counter = reset_counter_if_needed(role, counter)
29
+
30
+ cells = extract_cells(line, columns)
31
+
32
+ OpenStruct.new(cells: cells, role: role, counter: counter)
33
+ end
34
+ end
35
+
36
+ def update_role(current_role, line)
37
+ case current_role
38
+ when :header_row
39
+ if line =~ /^[ \t]*\| *[:\-][:\- |]*$/
40
+ :separator_line
41
+ else
42
+ current_role
43
+ end
44
+ when :separator_line
45
+ :row
46
+ when :row
47
+ current_role
48
+ else
49
+ raise "Unexpected role: #{current_role} for line #{line}"
50
+ end
51
+ end
52
+
53
+ def reset_counter_if_needed(role, counter)
54
+ %i[header_row row].include?(role) ? counter : 0
55
+ end
56
+
57
+ def extract_cells(line, columns)
58
+ cells = line.split('|').map(&:strip)[1..-1]
59
+ cells&.fill('', cells.length...columns)
60
+ end
61
+
62
+ def calculate_column_alignment_and_widths(rows, columns)
63
+ alignment_indicators = Array.new(columns, :left)
64
+ column_widths = Array.new(columns, 0)
65
+
66
+ rows.each do |row|
67
+ next if row.cells.nil?
68
+
69
+ row.cells.each_with_index do |cell, i|
70
+ column_widths[i] = [column_widths[i], cell.length].max
71
+
72
+ if row.role == :separator_line
73
+ alignment_indicators[i] = determine_column_alignment(cell)
74
+ end
75
+ end
76
+ end
77
+
78
+ # 2024-08-24 remove last column if it is 0-width
79
+ if column_widths.last.zero?
80
+ column_widths.pop
81
+ alignment_indicators.pop
82
+ end
83
+
84
+ [alignment_indicators, column_widths]
85
+ end
86
+
87
+ def determine_column_alignment(cell)
88
+ if cell =~ /^-+:$/
89
+ :right
90
+ elsif cell =~ /^:-+:$/
91
+ :center
92
+ else
93
+ :left
94
+ end
95
+ end
96
+
97
+ def format_rows(rows, alignment_indicators, column_widths, decorate)
98
+ rows.map do |row|
99
+ format_row_line(row, alignment_indicators, column_widths, decorate)
100
+ end
101
+ end
102
+
103
+ def format_row_line(row, alignment_indicators, column_widths, decorate,
104
+ text_sym: :text, style_sym: :color)
105
+ return '' if row.cells.nil?
106
+
107
+ border_style = decorate && decorate[:border]
108
+ HierarchyString.new(
109
+ [{ text_sym => '| ', style_sym => border_style },
110
+ *insert_every_other(
111
+ row.cells.map.with_index do |cell, i|
112
+ next unless alignment_indicators[i] && column_widths[i]
113
+
114
+ if row.role == :separator_line
115
+ { text_sym => '-' * column_widths[i],
116
+ style_sym => decorate && decorate[row.role] }
117
+ else
118
+ {
119
+ text_sym => format_cell(cell, alignment_indicators[i],
120
+ column_widths[i]),
121
+ style_sym => decoration_style(row.role, row.counter, decorate)
122
+ }
123
+ end
124
+ end.compact,
125
+ { text_sym => ' | ', style_sym => border_style }
126
+ ),
127
+ { text_sym => ' |', style_sym => border_style }].compact,
128
+ style_sym: style_sym,
129
+ text_sym: text_sym
130
+ ).decorate
131
+ end
132
+
133
+ def format_cell(cell, align, width)
134
+ case align
135
+ when :center
136
+ cell.center(width)
137
+ when :right
138
+ cell.rjust(width)
139
+ else
140
+ cell.ljust(width)
141
+ end
142
+ end
143
+
144
+ def decorate_line(line, role, counter, decorate)
145
+ return line unless decorate
146
+
147
+ return line unless (style = decoration_style(line, role, counter, decorate))
148
+
149
+ AnsiString.new(line).send(style)
150
+ end
151
+
152
+ def decoration_style(role, counter, decorate)
153
+ return nil unless decorate
154
+
155
+ return nil unless (style = decorate[role])
156
+
157
+ if style.is_a?(Array)
158
+ style[counter % style.count]
159
+ else
160
+ style
161
+ end
162
+ end
163
+
164
+ def insert_every_other(array, obj)
165
+ result = []
166
+ array.each_with_index do |element, index|
167
+ result << element
168
+ result << obj if index < array.size - 1
169
+ end
170
+ result
171
+ end
172
+ end
173
+
174
+ return if $PROGRAM_NAME != __FILE__
175
+
176
+ require 'minitest/autorun'
177
+ require_relative 'colorize'
178
+
179
+ class TestMarkdownTableFormatter < Minitest::Test
180
+ def setup
181
+ @lines = [
182
+ '| Header 1 | Header 2 | Header 3 |',
183
+ '|----------|:--------:|---------:|',
184
+ '| Row 1 Col 1 | Row 1 Col 2 | Row 1 Col 3 |',
185
+ '| Row 2 Col 1 | Row 2 Col 2 | Row 2 Col 3 |'
186
+ ]
187
+ @columns = 3
188
+ end
189
+
190
+ def test_format_table
191
+ result = MarkdownTableFormatter.format_table(@lines, @columns)
192
+ expected = [
193
+ '| Header 1 | Header 2 | Header 3 |',
194
+ '| ----------- | ----------- | ----------- |',
195
+ '| Row 1 Col 1 | Row 1 Col 2 | Row 1 Col 3 |',
196
+ '| Row 2 Col 1 | Row 2 Col 2 | Row 2 Col 3 |'
197
+ ]
198
+ assert_equal expected, result
199
+ end
200
+
201
+ def test_format_table_with_decoration
202
+ decorate = { header_row: :upcase, row: %i[downcase upcase] }
203
+ result = MarkdownTableFormatter.format_table(@lines, @columns,
204
+ decorate: decorate)
205
+ expected = [
206
+ '| HEADER 1 | HEADER 2 | HEADER 3 |',
207
+ '| ----------- | ----------- | ----------- |',
208
+ '| ROW 1 COL 1 | ROW 1 COL 2 | ROW 1 COL 3 |',
209
+ '| row 2 col 1 | row 2 col 2 | row 2 col 3 |'
210
+ ]
211
+ assert_equal expected, result
212
+ end
213
+
214
+ def test_format_table_with_empty_lines
215
+ lines_with_empty = [
216
+ '| Header 1 | Header 2 | Header 3 |',
217
+ '|----------|:--------:|---------:|',
218
+ '| Row 1 Col 1 | Row 1 Col 2 | Row 1 Col 3 |',
219
+ '',
220
+ '| Row 2 Col 1 | Row 2 Col 2 | Row 2 Col 3 |'
221
+ ]
222
+ result = MarkdownTableFormatter.format_table(lines_with_empty, @columns)
223
+ expected = [
224
+ '| Header 1 | Header 2 | Header 3 |',
225
+ '| ----------- | ----------- | ----------- |',
226
+ '| Row 1 Col 1 | Row 1 Col 2 | Row 1 Col 3 |',
227
+ '',
228
+ '| Row 2 Col 1 | Row 2 Col 2 | Row 2 Col 3 |'
229
+ ]
230
+ assert_equal expected, result
231
+ end
232
+
233
+ def test_alignment_detection
234
+ lines_with_alignment = [
235
+ '| Header 1 | Header 2 | Header 3 |',
236
+ '|:-------- |:--------:| --------:|'
237
+ ]
238
+ result = MarkdownTableFormatter.format_table(lines_with_alignment, @columns)
239
+ expected = [
240
+ '| Header 1 | Header 2 | Header 3 |',
241
+ '| --------- | ---------- | --------- |'
242
+ ]
243
+ assert_equal expected, result[0..1] # only checking the header and separator
244
+ end
245
+ end
246
+
247
+ class TestFormatTable < Minitest::Test
248
+ def test_basic_formatting
249
+ lines = [
250
+ '| Species| Genus| Family',
251
+ '|-|-|-',
252
+ '| Pongo tapanuliensis| Pongo| Hominidae',
253
+ '| | Histiophryne| Antennariidae'
254
+ ]
255
+ columns = 3
256
+ expected = [
257
+ '| Species | Genus | Family |',
258
+ '| ------------------- | ------------ | ------------- |',
259
+ '| Pongo tapanuliensis | Pongo | Hominidae |',
260
+ '| | Histiophryne | Antennariidae |'
261
+ ]
262
+ assert_equal expected, MarkdownTableFormatter.format_table(lines, columns)
263
+ end
264
+
265
+ def test_missing_columns
266
+ lines = [
267
+ '| A| B| C',
268
+ '| 1| 2',
269
+ '| X'
270
+ ]
271
+ columns = 3
272
+ expected = [
273
+ '| A | B | C |',
274
+ '| 1 | 2 | |',
275
+ '| X | | |'
276
+ ]
277
+ assert_equal expected, MarkdownTableFormatter.format_table(lines, columns)
278
+ end
279
+
280
+ # def test_extra_columns
281
+ # lines = [
282
+ # "| A| B| C| D",
283
+ # "| 1| 2| 3| 4| 5"
284
+ # ]
285
+ # columns = 3
286
+ # expected = [
287
+ # "| A | B | C ",
288
+ # "| 1 | 2 | 3 "
289
+ # ]
290
+ # assert_equal expected, MarkdownTableFormatter.format_table(lines, columns)
291
+ # end
292
+
293
+ def test_empty_input
294
+ assert_equal [], MarkdownTableFormatter.format_table([], 3)
295
+ end
296
+
297
+ def test_single_column
298
+ lines = [
299
+ '| A',
300
+ '| Longer text',
301
+ '| Short'
302
+ ]
303
+ columns = 1
304
+ expected = [
305
+ '| A |',
306
+ '| Longer text |',
307
+ '| Short |'
308
+ ]
309
+ assert_equal expected, MarkdownTableFormatter.format_table(lines, columns)
310
+ end
311
+
312
+ def test_no_pipe_at_end
313
+ lines = [
314
+ '| A| B| C',
315
+ '| 1| 2| 3'
316
+ ]
317
+ columns = 3
318
+ expected = [
319
+ '| A | B | C |',
320
+ '| 1 | 2 | 3 |'
321
+ ]
322
+ assert_equal expected, MarkdownTableFormatter.format_table(lines, columns)
323
+ end
324
+ end
325
+
326
+ class TestFormatTable2 < Minitest::Test
327
+ def test_basic_formatting
328
+ lines = [
329
+ '| Name | Age | City |',
330
+ '| John | 30 | New York |',
331
+ '| Jane | 25 | Los Angeles |'
332
+ ]
333
+ expected_output = [
334
+ '| Name | Age | City |',
335
+ '| John | 30 | New York |',
336
+ '| Jane | 25 | Los Angeles |'
337
+ ]
338
+ assert_equal expected_output, MarkdownTableFormatter.format_table(lines, 3)
339
+ end
340
+
341
+ def test_incomplete_columns
342
+ lines = [
343
+ '| Name | Age |',
344
+ '| John | 30 | New York |',
345
+ '| Jane | 25 | Los Angeles |'
346
+ ]
347
+ expected_output = [
348
+ '| Name | Age | |',
349
+ '| John | 30 | New York |',
350
+ '| Jane | 25 | Los Angeles |'
351
+ ]
352
+ assert_equal expected_output, MarkdownTableFormatter.format_table(lines, 3)
353
+ end
354
+
355
+ def test_extra_columns
356
+ lines = [
357
+ '| Name | Age | City | Country |',
358
+ '| John | 30 | New York | USA |',
359
+ '| Jane | 25 | Los Angeles | USA |'
360
+ ]
361
+ expected_output = [
362
+ '| Name | Age | City | Country |',
363
+ '| John | 30 | New York | USA |',
364
+ '| Jane | 25 | Los Angeles | USA |'
365
+ ]
366
+ assert_equal expected_output, MarkdownTableFormatter.format_table(lines, 4)
367
+ end
368
+
369
+ def test_varied_column_lengths
370
+ lines = [
371
+ '| Name | Age |',
372
+ '| Johnathan | 30 | New York |',
373
+ '| Jane | 25 | LA |'
374
+ ]
375
+ expected_output = [
376
+ '| Name | Age | |',
377
+ '| Johnathan | 30 | New York |',
378
+ '| Jane | 25 | LA |'
379
+ ]
380
+ assert_equal expected_output, MarkdownTableFormatter.format_table(lines, 3)
381
+ end
382
+
383
+ def test_single_line
384
+ lines = ['| Name | Age | City |']
385
+ expected_output = ['| Name | Age | City |']
386
+ assert_equal expected_output, MarkdownTableFormatter.format_table(lines, 3)
387
+ end
388
+
389
+ def test_empty_lines
390
+ lines = []
391
+ expected_output = []
392
+ assert_equal expected_output, MarkdownTableFormatter.format_table(lines, 3)
393
+ end
394
+
395
+ def test_complete_rows
396
+ lines = [
397
+ '| Name | Age |',
398
+ '| John | 30 |'
399
+ ]
400
+ expected_output = [
401
+ '| Name | Age |',
402
+ '| John | 30 |'
403
+ ]
404
+ assert_equal expected_output, MarkdownTableFormatter.format_table(lines, 3)
405
+ end
406
+ end