markdown_exec 2.2.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +16 -4
- data/CHANGELOG.md +28 -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 +40 -5
- 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 +41 -0
- 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 +939 -611
- data/lib/hierarchy_string.rb +221 -0
- data/lib/input_sequencer.rb +19 -11
- 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 +420 -165
- data/lib/mdoc.rb +38 -38
- data/lib/menu.src.yml +832 -680
- data/lib/menu.yml +814 -689
- 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 +236 -0
- metadata +28 -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
|