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
@@ -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