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.
- checksums.yaml +4 -4
- data/.rubocop.yml +11 -2
- data/CHANGELOG.md +19 -0
- data/Gemfile.lock +1 -1
- data/Rakefile +32 -8
- data/bats/bats.bats +33 -0
- data/bats/block-types.bats +56 -0
- data/bats/cli.bats +74 -0
- data/bats/fail.bats +11 -0
- data/bats/history.bats +34 -0
- data/bats/markup.bats +66 -0
- data/bats/mde.bats +29 -0
- data/bats/options.bats +92 -0
- data/bats/test_helper.bash +152 -0
- data/bin/tab_completion.sh +44 -20
- data/docs/dev/block-type-opts.md +10 -0
- data/docs/dev/block-type-port.md +24 -0
- data/docs/dev/block-type-vars.md +7 -0
- data/docs/dev/pass-through-arguments.md +8 -0
- data/docs/dev/specs-import.md +9 -0
- data/docs/dev/specs.md +83 -0
- data/docs/dev/text-decoration.md +7 -0
- data/examples/bash-blocks.md +4 -4
- data/examples/block-names.md +2 -2
- data/examples/import0.md +23 -0
- data/examples/import1.md +13 -0
- data/examples/link-blocks-vars.md +3 -3
- data/examples/opts-blocks-require.md +6 -6
- data/examples/table-markup.md +31 -0
- data/examples/text-markup.md +58 -0
- data/examples/vars-blocks.md +2 -2
- data/examples/wrap.md +87 -9
- data/lib/ansi_formatter.rb +12 -6
- data/lib/ansi_string.rb +153 -0
- data/lib/argument_processor.rb +160 -0
- data/lib/cached_nested_file_reader.rb +4 -2
- data/lib/ce_get_cost_and_usage.rb +4 -3
- data/lib/cli.rb +1 -1
- data/lib/colorize.rb +39 -11
- data/lib/constants.rb +17 -0
- data/lib/directory_searcher.rb +4 -2
- data/lib/doh.rb +190 -0
- data/lib/env.rb +1 -1
- data/lib/exceptions.rb +9 -6
- data/lib/fcb.rb +0 -199
- data/lib/filter.rb +18 -5
- data/lib/find_files.rb +8 -3
- data/lib/format_table.rb +406 -0
- data/lib/hash_delegator.rb +888 -603
- data/lib/hierarchy_string.rb +113 -25
- data/lib/input_sequencer.rb +16 -10
- data/lib/instance_method_wrapper.rb +2 -1
- data/lib/layered_hash.rb +143 -0
- data/lib/link_history.rb +22 -8
- data/lib/markdown_exec/version.rb +1 -1
- data/lib/markdown_exec.rb +413 -165
- data/lib/mdoc.rb +27 -34
- data/lib/menu.src.yml +825 -710
- data/lib/menu.yml +799 -703
- data/lib/namer.rb +6 -12
- data/lib/object_present.rb +1 -1
- data/lib/option_value.rb +7 -3
- data/lib/poly.rb +33 -14
- data/lib/resize_terminal.rb +60 -52
- data/lib/saved_assets.rb +45 -34
- data/lib/saved_files_matcher.rb +6 -3
- data/lib/streams_out.rb +7 -1
- data/lib/table_extractor.rb +166 -0
- data/lib/tap.rb +5 -6
- data/lib/text_analyzer.rb +144 -8
- metadata +26 -3
- data/lib/std_out_err_logger.rb +0 -119
data/lib/format_table.rb
ADDED
@@ -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
|