Skip to content
This repository was archived by the owner on Jan 14, 2022. It is now read-only.

Commit 0766abe

Browse files
Big big refactor
1 parent 265a7d5 commit 0766abe

File tree

4 files changed

+150
-166
lines changed

4 files changed

+150
-166
lines changed

.appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ install:
1313

1414
build: off
1515

16-
test_script: test\unittest.rb
16+
test_script: test\unittest.rb

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
[![Travis CI](https://travis-ci.org/christopher-dG/markdown-tables.svg?branch=master)](https://travis-ci.org/christopher-dG/markdown-tables)
55
[![AppVeyor](https://ci.appveyor.com/api/projects/status/sfqh4ouq46qjuxvx/branch/master?svg=true)](https://ci.appveyor.com/project/christopher-dG/markdown-tables/branch/master)
66
[![Codecov](https://codecov.io/gh/christopher-dG/markdown-tables/branch/master/graph/badge.svg)](https://codecov.io/gh/christopher-dG/markdown-tables)
7-
7+
[![Code Climate](https://codeclimate.com/github/christopher-dG/markdown-tables.png)](https://codeclimate.com/github/christopher-dG/markdown-tables)
88

99
**Utilities for creating and displaying Markdown tables in Ruby.**
1010

lib/markdown-tables.rb

Lines changed: 85 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -6,92 +6,75 @@ class MarkdownTables
66
# Pass align: 'l' for left alignment or 'r' for right alignment. Anything
77
# else will result in cells being centered. Pass an array of align values
88
# to specify alignment per column.
9-
# If is_rows is true, then each sub-array represents a row.
9+
# If is_rows is true, then each sub-array of data represents a row.
1010
# Conversely, if is_rows is false, each sub-array of data represents a column.
1111
# Empty cells can be given with nil or an empty string.
1212
def self.make_table(labels, data, align: '', is_rows: false)
13+
validate(labels, data, align, is_rows)
14+
15+
# Deep copy the arguments so we don't mutate the originals.
1316
labels = Marshal.load(Marshal.dump(labels))
1417
data = Marshal.load(Marshal.dump(data))
15-
validate(labels, data, align, is_rows)
16-
sanitize!(labels, data)
1718

19+
# Remove any breaking Markdown characters.
20+
labels.map! {|label| sanitize(label)}
21+
data.map! {|datum| datum.map {|cell| sanitize(cell)}}
22+
23+
# Convert align to something that other methods won't need to validate.
24+
align.class == String && align = [align] * labels.length
25+
align.map! {|a| a =~ /[lr]/i ? a.downcase : 'c'}
26+
27+
# Generate the column labels and alignment line.
1828
header_line = labels.join('|')
19-
alignment_line = alignment(align, labels.length)
20-
21-
if is_rows
22-
rows = data.map {|row| row.join('|')}
23-
else
24-
max_len = data.map(&:size).max
25-
rows = []
26-
max_len.times do |i|
27-
row = []
28-
data.each {|col| row.push(col[i])}
29-
rows.push(row.join('|'))
30-
end
31-
end
29+
alignment_line = parse_alignment(align, labels.length)
30+
31+
# Pad the data arrays so that it can be transposed if necessary.
32+
max_len = data.map(&:length).max
33+
data.map! {|datum| fill(datum, max_len)}
34+
35+
# Generate the table rows.
36+
rows = (is_rows ? data : data.transpose).map {|row| row.join('|')}
3237

3338
return [header_line, alignment_line, rows.join("\n")].join("\n")
3439
end
3540

3641
# Convert a Markdown table into human-readable form.
3742
def self.plain_text(md_table)
43+
md_table !~ // && raise('Invalid input')
44+
45+
# Split the table into lines to get the labels, rows, and alignments.
3846
lines = md_table.split("\n")
3947
alignments = lines[1].split('|')
48+
# labels or rows might have some empty values but alignments
49+
# is guaranteed to be of the right width.
4050
table_width = alignments.length
41-
42-
# Add back any any missing empty cells.
43-
labels = lines[0].split('|')
44-
labels.length < table_width && labels += [' '] * (table_width - labels.length)
45-
rows = lines[2..-1].map {|line| line.split('|')}
46-
rows.each_index do |i|
47-
rows[i].length < table_width && rows[i] += [' '] * (table_width - rows[i].length)
48-
end
49-
50-
# Replace non-breaking HTML characters with their plaintext counterparts.
51-
rows.each do |row|
52-
row.each do |cell|
53-
cell.gsub!(/(&nbsp;)|(&#124;)/, '&nbsp;' => ' ', '&#124;' => '|')
54-
end
55-
end
51+
# '|||'.split('|') == [], so we need to manually add trailing empty cells.
52+
# Leading empty cells are taken care of automatically.
53+
labels = fill(lines[0].split('|'), table_width)
54+
rows = lines[2..-1].map {|line| fill(line.split('|'), table_width)}
5655

5756
# Get the width for each column.
58-
widths = labels.map(&:length) # Lengths of each column's longest element.
59-
rows.length.times do |i|
60-
rows[i].length.times do |j|
61-
rows[i][j].length > widths[j] && widths[j] = rows[i][j].length
62-
end
63-
end
64-
widths.map! {|w| w + 2} # Add padding on each side.
65-
66-
# Align the column labels.
67-
labels.length.times do |i|
68-
label_length = labels[i].length
69-
start = align_cell(label_length, widths[i], alignments[i])
70-
71-
labels[i].prepend(' ' * start)
72-
labels[i] += ' ' * (widths[i] - start - label_length)
73-
end
74-
75-
# Align the cells.
76-
rows.each do |row|
77-
row.length.times do |i|
78-
cell_length = row[i].length
79-
start = align_cell(cell_length, widths[i], alignments[i])
80-
row[i].prepend(' ' * start)
81-
row[i] += ' ' * (widths[i] - start - cell_length)
82-
end
83-
end
57+
cols = rows.transpose
58+
widths = cols.each_index.map {|i| column_width(cols[i].push(labels[i]))}
59+
60+
# Align the labels and cells.
61+
labels = labels.each_index.map { |i|
62+
aligned_cell(unsanitize(labels[i]), widths[i], alignments[i])
63+
}
64+
rows.map! { |row|
65+
row.each_index.map { |i|
66+
aligned_cell(unsanitize(row[i]), widths[i], alignments[i])
67+
}
68+
}
8469

8570
border = "\n|" + widths.map {|w| '=' * w}.join('|') + "|\n"
86-
separator = border.gsub('=', '-')
87-
88-
table = border[1..-1] # Don't include the first newline.
89-
table += '|' + labels.join('|') + '|'
90-
table += border
91-
table += rows.map {|row| '|' + row.join('|') + '|'}.join(separator)
92-
table += border
71+
return (
72+
border + [
73+
'|' + labels.join('|') + '|',
74+
rows.map {|row| '|' + row.join('|') + '|'}.join(border.tr('=', '-'))
75+
].join(border) + border
76+
).strip
9377

94-
return table.chomp
9578
end
9679

9780
# Sanity checks for make_table.
@@ -119,43 +102,51 @@ def self.plain_text(md_table)
119102
end
120103
end
121104

122-
# Convert all input to strings and replace any '|' characters with
123-
# non-breaking equivalents,
124-
private_class_method def self.sanitize!(labels, data)
105+
# Convert some input to a string and replace any '|' characters with
106+
# a non-breaking equivalent,
107+
private_class_method def self.sanitize(input)
125108
bar = '&#124;' # Non-breaking HTML vertical bar.
126-
labels.map! {|label| label.to_s.gsub('|', bar)}
127-
data.length.times {|i| data[i].map! {|cell| cell.to_s.gsub('|', bar)}}
109+
return input.to_s.gsub('|', bar)
110+
end
111+
112+
# Replace non-breaking HTML characters with their plaintext counterparts.
113+
private_class_method def self.unsanitize(input)
114+
return input.gsub(/(&nbsp;)|(&#124;)/, '&nbsp;' => ' ', '&#124;' => '|')
128115
end
129116

130117
# Generate the alignment line from a string or array.
131-
# align must be a string or array or strings.
118+
# align must be a string or array of strings.
132119
# n: number of labels in the table to be created.
133-
private_class_method def self.alignment(align, n)
134-
if align.class == String
135-
alignment = align == 'l' ? ':-' : align == 'r' ? '-:' : ':-:'
136-
alignment_line = ([alignment] * n).join('|')
137-
else
138-
alignments = align.map {
139-
|a| a.downcase == 'l' ? ':-' : a.downcase == 'r' ? '-:' : ':-:'
140-
}
141-
if alignments.length < n
142-
alignments += [':-:'] * (n - alignments.length)
143-
end
144-
alignment_line = alignments.join('|')
145-
end
146-
return alignment_line
120+
private_class_method def self.parse_alignment(align, n)
121+
align_map = {'l' => ':-', 'c' => ':-:', 'r' => '-:'}
122+
alignments = align.map {|a| align_map[a]}
123+
# If not enough values were given, center the remaining columns.
124+
alignments.length < n && alignments += [':-:'] * (n - alignments.length)
125+
return alignments.join('|')
147126
end
148127

149-
# Get the starting index of a cell's text from the text's length, the cell's
150-
# width, and the alignment.
151-
private_class_method def self.align_cell(length, width, align)
152-
if align =~ /:-+:/
153-
return (width / 2) - (length / 2)
154-
elsif align =~ /-+:/
155-
return width - length - 1
156-
else
157-
return 1
128+
# Align some text in a cell.
129+
private_class_method def self.aligned_cell(text, width, align)
130+
if align =~ /:-+:/ # Center alignment.
131+
start = (width / 2) - (text.length / 2)
132+
elsif align =~ /-+:/ # Right alignment.
133+
start = width - text.length - 1
134+
else # Left alignment.
135+
start = 1
158136
end
137+
return ' ' * start + text + ' ' * (width - start - text.length)
138+
end
139+
140+
# Get the width for a column.
141+
private_class_method def self.column_width(col)
142+
# Pad each cell on either side and maintain a minimum 3 width of characters.
143+
return [(!col.empty? ? col.map(&:length).max : 0) + 2, 3].max
144+
end
145+
146+
# Add any missing empty values to a row.
147+
private_class_method def self.fill(row, n)
148+
row.length > n && raise('Sanity checks failed for fill')
149+
return row.length < n ? row + ([''] * (n - row.length)) : row
159150
end
160151

161152
end

0 commit comments

Comments
 (0)