@@ -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! ( /( )|(|)/ , ' ' => ' ' , '|' => '|' )
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 = '|' # 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 ( /( )|(|)/ , ' ' => ' ' , '|' => '|' )
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
161152end
0 commit comments