marktable 0.0.5 → 0.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a3e8c1a76daf4c3d03f628200baabad28e31008e444522fb2d6c7337f623bdd8
4
- data.tar.gz: c83d7b0a83b21c2c292d5a76963f626ddeae5bebca42363f227f6bf65fed6dc2
3
+ metadata.gz: 776f59a6e0c0ee0b8e52f3fd4b9dd7a8ae8e1acbb93ff98504bb382dfecc93e9
4
+ data.tar.gz: 8df1d4b723562ced9f83e172165ed07d934e95fe2d9fe7c39411371b960dadfb
5
5
  SHA512:
6
- metadata.gz: f0f766d34db37402b6d4cbb52e37a1e33172175c5ba622051ce9899ded65de3c9fa045f7d21a234c9fc03856bfaefcf7b9b8d5f1fb346bd0c47efbee87e86236
7
- data.tar.gz: 1a34281c031a0c47da3ff074f6206304fc569663a04ffab9246bd12a137eb7fd53a6745415991d6530d384f67e05eca480db02bdd9c49122687a9ae820475037
6
+ metadata.gz: ed5b2e94ca382771d81d5c6423fa43e38dded1cbfe315c6fedd098b0f77efc66d18b7e62372f8ac2e16884a3d3495f5631fee13b96623d8575276e8b6827b619
7
+ data.tar.gz: 28d859a6e6c1709ee0be5c3ff7f63fd91f87688d6c5169d0193790889e590ce4b0602afa7c1a5f48693227bf963cca9396a26900f037899351d4c54c69783833
data/README.md CHANGED
@@ -1,6 +1,24 @@
1
1
  # Marktable
2
2
 
3
- Marktable is a Ruby gem for ...
3
+ Marktable is a Ruby gem for easily converting between different table formats and testing table content in your specs.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Features](#features)
8
+ - [Installation](#installation)
9
+ - [Basic Usage](#basic-usage)
10
+ - [Documentation](#documentation)
11
+ - [License](#license)
12
+
13
+ ## Features
14
+
15
+ * RSpec matchers to compare tables across formats
16
+ * Convert between multiple table formats:
17
+ - Markdown tables
18
+ - Arrays of arrays
19
+ - Arrays of hashes
20
+ - CSV
21
+ - HTML tables
4
22
 
5
23
  ## Installation
6
24
 
@@ -12,23 +30,59 @@ gem 'marktable'
12
30
 
13
31
  And then execute:
14
32
 
15
- ```bash
16
- bundle install
33
+ ```
34
+ $ bundle install
35
+ ```
36
+
37
+ Or install it yourself:
38
+
39
+ ```
40
+ $ gem install marktable
17
41
  ```
18
42
 
19
- Or install it yourself as:
43
+ ## Basic Usage
20
44
 
21
- ```bash
22
- gem install marktable
45
+ ### RSpec Matchers
46
+
47
+ ```ruby
48
+ # In your spec_helper.rb
49
+ require 'marktable/rspec'
50
+
51
+ # In your specs - compare tables across formats
52
+ expect(markdown_table).to match_markdown(expected_markdown)
53
+
54
+ html_table = page.find('#users-table')
55
+ expect(html_table).to match_markdown(expected_markdown)
23
56
  ```
24
57
 
25
- ## Usage
58
+ ### Converting Between Formats
26
59
 
27
60
  ```ruby
28
- require 'marktable'
29
- # Example usage here
61
+ # From markdown to other formats
62
+ markdown = <<~MARKDOWN
63
+ | Name | Age |
64
+ |------|----- |
65
+ | Alice | 30 |
66
+ | Bob | 25 |
67
+ MARKDOWN
68
+
69
+ table = Marktable.from_markdown(markdown)
70
+
71
+ table.to_a # Array of hashes
72
+ table.to_csv # CSV string
73
+ table.to_html # HTML table
74
+
75
+ # From arrays or hashes to markdown
76
+ data = [{ 'Name' => 'Alice', 'Age' => '30' }]
77
+ Marktable.from_array(data).to_md # Markdown table
30
78
  ```
31
79
 
32
- ## Development
80
+ ## Documentation
81
+
82
+ * [Full Examples](docs/examples.md) - Detailed usage examples
83
+ * [API Documentation](docs/api_documentation.md) - Complete API reference
84
+
85
+ ## License
33
86
 
34
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
87
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
88
+ `
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'markdown'
4
+ # require_relative "array"
5
+ require_relative 'csv'
6
+ require_relative 'html'
7
+
8
+ module Marktable
9
+ module Formatters
10
+ class Base
11
+ def self.for(type)
12
+ case type.to_sym
13
+ when :markdown
14
+ Markdown
15
+ when :array
16
+ Array
17
+ when :csv
18
+ CSV
19
+ when :html
20
+ HTML
21
+ else
22
+ raise ArgumentError, "Unknown table type: #{type}"
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'csv'
4
+
5
+ module Marktable
6
+ module Formatters
7
+ class CSV
8
+ def self.format(rows, headers = nil)
9
+ return '' if rows.empty? && headers.nil?
10
+
11
+ ::CSV.generate do |csv|
12
+ csv << headers if headers
13
+ rows.each do |row|
14
+ csv << format_row(row)
15
+ end
16
+ end
17
+ end
18
+
19
+ def self.format_row(row)
20
+ row.values.map { |val| val unless val.to_s.empty? }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+
5
+ module Marktable
6
+ module Formatters
7
+ class HTML
8
+ def self.format(rows, headers = nil)
9
+ return '' if rows.empty? && headers.nil?
10
+
11
+ builder = Nokogiri::HTML::Builder.new do |doc|
12
+ doc.table do
13
+ if headers
14
+ doc.thead do
15
+ doc.tr do
16
+ headers.each do |header|
17
+ doc.th { doc.text header }
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ doc.tbody do
24
+ rows.each do |row|
25
+ doc.tr do
26
+ row.values.each do |cell|
27
+ doc.td do
28
+ cell_text = cell.to_s
29
+ if cell_text.include?('\n')
30
+ cell_text.split('\n').each_with_index do |line, index|
31
+ doc.br if index.positive?
32
+ doc.text line
33
+ end
34
+ else
35
+ doc.text cell_text
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ # Extract just the table element to avoid including DOCTYPE
46
+ builder.doc.at_css('table').to_html
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Marktable
4
+ module Formatters
5
+ class Markdown
6
+ def self.format(rows, headers = nil)
7
+ return '' if rows.empty? && headers.nil?
8
+
9
+ # Calculate column widths
10
+ widths = calculate_column_widths(rows, headers)
11
+
12
+ lines = []
13
+
14
+ # Add header row if we have headers
15
+ if headers
16
+ lines << Row.new(headers).to_markdown(widths)
17
+ lines << separator_row(widths)
18
+ end
19
+
20
+ # Add data rows
21
+ rows.each do |row|
22
+ lines << row.to_markdown(widths)
23
+ end
24
+
25
+ lines.join("\n")
26
+ end
27
+
28
+ def self.calculate_column_widths(rows, headers)
29
+ # Determine the maximum number of columns to consider
30
+ max_cols = headers ? headers.size : 0
31
+
32
+ if headers.nil?
33
+ # Without headers, find the maximum number of values across all rows
34
+ rows.each do |row|
35
+ max_cols = [max_cols, row.values.size].max
36
+ end
37
+ end
38
+
39
+ # Initialize widths array with zeros
40
+ widths = Array.new(max_cols, 0)
41
+
42
+ # Process headers if available
43
+ headers&.each_with_index do |header, i|
44
+ width = header.to_s.length
45
+ widths[i] = width if width > widths[i]
46
+ end
47
+
48
+ # Process row values, but only up to the max_cols
49
+ rows.each do |row|
50
+ values = row.values.take(max_cols)
51
+ values.each_with_index do |value, i|
52
+ width = value.to_s.length
53
+ widths[i] = width if width > widths[i]
54
+ end
55
+ end
56
+
57
+ widths
58
+ end
59
+
60
+ def self.separator_row(widths)
61
+ separator_parts = widths.map { |width| '-' * width }
62
+ "| #{separator_parts.join(' | ')} |"
63
+ end
64
+ end
65
+ end
66
+ end
data/lib/marktable/row.rb CHANGED
@@ -2,97 +2,109 @@
2
2
 
3
3
  module Marktable
4
4
  class Row
5
- attr_reader :data, :headers
5
+ attr_reader :values, :headers
6
6
 
7
- def initialize(data = {}, headers: nil)
7
+ def initialize(data, headers: nil)
8
8
  @headers = headers
9
-
10
- if data.is_a?(Hash)
11
- # Ensure all hash values are strings
12
- @data = data.transform_values(&:to_s)
13
- elsif data.is_a?(Array)
14
- # Ensure all array elements are strings
15
- data_strings = data.map(&:to_s)
16
-
17
- @data = if headers && !headers.empty?
18
- # Convert array to hash using headers
19
- headers.each_with_index.each_with_object({}) do |(header, i), hash|
20
- hash[header] = i < data_strings.length ? data_strings[i] : ''
21
- end
9
+ @values = extract_values(data)
10
+ end
11
+
12
+ # Format the row as a markdown table row
13
+ def to_markdown(column_widths)
14
+ vals = @values
15
+
16
+ # Limit values to either headers count or column_widths size, whichever is appropriate
17
+ max_cols = @headers ? @headers.size : column_widths.size
18
+
19
+ formatted_values = column_widths.take(max_cols).map.with_index do |width, i|
20
+ if i < vals.size
21
+ vals[i].to_s.ljust(width)
22
22
  else
23
- # Keep as array when no headers
24
- data_strings
23
+ ''.ljust(width)
25
24
  end
26
- else
27
- @data = headers ? {} : []
28
25
  end
26
+
27
+ "| #{formatted_values.join(' | ')} |"
29
28
  end
30
29
 
30
+ # Access a value by index or header
31
31
  def [](key)
32
- if @data.is_a?(Hash)
33
- @data[key]
34
- elsif key.is_a?(Integer) && key < @data.length
35
- @data[key]
36
- else
37
- nil
32
+ if key.is_a?(Integer)
33
+ @values[key]
34
+ elsif @headers
35
+ idx = @headers.index(key)
36
+ idx ? @values[idx] : nil
38
37
  end
39
38
  end
40
39
 
40
+ # Set a value by index or header
41
41
  def []=(key, value)
42
- if @data.is_a?(Hash)
43
- @data[key] = value.to_s
44
- elsif key.is_a?(Integer)
45
- @data[key] = value.to_s
42
+ if key.is_a?(Integer)
43
+ @values[key] = value if key >= 0 && key < @values.size
44
+ elsif @headers
45
+ idx = @headers.index(key)
46
+ @values[idx] = value if idx && idx < @values.size
46
47
  end
47
48
  end
48
49
 
49
- def values
50
- @data.is_a?(Hash) ? @data.values : @data
51
- end
52
-
53
- def keys
54
- @data.is_a?(Hash) ? @data.keys : (0...@data.size).to_a
55
- end
56
-
57
- def to_h
58
- return @data if @data.is_a?(Hash)
59
- return {} if @data.empty? || @headers.nil? || @headers.empty?
60
-
61
- @headers.each_with_index.each_with_object({}) do |(header, i), hash|
62
- hash[header] = i < @data.length ? @data[i] : ''
63
- end
50
+ # Check if this row uses headers
51
+ def headers?
52
+ !@headers.nil?
64
53
  end
65
54
 
66
- def to_a
67
- @data.is_a?(Array) ? @data : @data.values
68
- end
55
+ # Convert row data to a hash using headers as keys
56
+ def to_hash
57
+ return {} unless @headers
69
58
 
70
- # Convert a row to markdown format with specified column widths
71
- def to_markdown(column_widths)
72
- vals = values
73
- formatted_values = vals.each_with_index.map do |val, i|
74
- val.to_s.ljust(column_widths[i] || val.to_s.length)
59
+ result = {}
60
+ @values.each_with_index do |value, i|
61
+ # Only include values that have a corresponding header
62
+ if i < @headers.size
63
+ header = @headers[i]
64
+ result[header] = value if header
65
+ end
75
66
  end
76
- "| #{formatted_values.join(' | ')} |"
67
+ result
77
68
  end
78
69
 
79
70
  # Parse a markdown row string into an array of values
80
71
  def self.parse(row_string)
81
- row_string.strip.sub(/^\|/, '').sub(/\|$/, '').split('|').map(&:strip)
72
+ # Skip if nil or empty
73
+ return [] if row_string.nil? || row_string.strip.empty?
74
+
75
+ # Remove leading/trailing pipes and split by pipe
76
+ cells = row_string.strip.sub(/^\|/, '').sub(/\|$/, '').split('|')
77
+
78
+ # Trim whitespace from each cell
79
+ cells.map(&:strip)
82
80
  end
83
81
 
84
- # Check if a row string represents a separator row
82
+ # Check if a row is a separator row
85
83
  def self.separator?(row_string)
86
- row_string.strip.gsub(/[\|\-\s]/, '').empty?
84
+ # Skip if nil or empty
85
+ return false if row_string.nil? || row_string.strip.empty?
86
+
87
+ # Remove pipes and strip whitespace
88
+ content = row_string.gsub('|', '').strip
89
+
90
+ # Check if it contains only dashes and colons (separator characters)
91
+ content.match?(/^[\s:,-]+$/) && content.include?('-')
87
92
  end
88
93
 
89
- # Generate a separator row for markdown table with specified widths
90
- def self.separator_row(column_widths)
91
- separators = column_widths.map do |width|
92
- '-' * [3, width].max
94
+ private
95
+
96
+ def extract_values(data)
97
+ case data
98
+ when Hash
99
+ if @headers
100
+ @headers.map { |h| data[h] || '' }
101
+ else
102
+ data.values
103
+ end
104
+ else
105
+ # Array or other enumerable
106
+ Array(data)
93
107
  end
94
-
95
- ["| #{separators.join(' | ')} |", column_widths]
96
108
  end
97
109
  end
98
110
  end
@@ -1,154 +1,84 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'formatters/base'
4
+
3
5
  module Marktable
4
6
  class Table
5
7
  include Enumerable
6
-
8
+
7
9
  attr_reader :headers
8
10
 
9
- def initialize(markdown_table = '', headers: true)
10
- @headers = headers
11
- @rows = []
12
- @header_row = nil
13
- parse_content(markdown_table) unless markdown_table.empty?
11
+ def initialize(source, type: :markdown, headers: nil)
12
+ parser = Tables::Base.for(type).new(source, headers)
13
+ result = parser.parse
14
+ @rows = result.rows
15
+ @headers = result.headers
16
+ # Validate headers if present
17
+ validate_headers if @headers
18
+ # Fix: Initialize @has_headers based on whether @headers is present
19
+ @has_headers = !@headers.nil?
14
20
  end
15
21
 
16
- def each
17
- if block_given?
18
- @rows.each { |row| yield(row) }
19
- else
20
- @rows.each
21
- end
22
- end
22
+ # Iteration support
23
+ def each(&block)
24
+ return enum_for(:each) unless block_given?
23
25
 
24
- def to_a
25
- @rows.map { |row| row.data }
26
- end
27
-
28
- def to_s
29
- generate
26
+ @rows.each(&block)
30
27
  end
31
28
 
32
- def generate
33
- return "" if @rows.empty?
34
-
35
- # Extract header keys or use first row data for header
36
- keys = header_keys
37
-
38
- # Calculate column widths considering both headers and all row values
39
- all_values = [keys] + @rows.map { |row| row.values }
40
- column_widths = calculate_column_widths(all_values)
41
-
42
- # Build the markdown table
43
- build_markdown_table(keys, column_widths)
44
- end
45
-
46
- private
47
-
48
- def header_keys
49
- if @headers && @header_row
50
- @header_row.keys
51
- elsif @rows.first&.data.is_a?(Hash)
52
- @rows.first.data.keys
29
+ # Returns the table as an Array of Hashes if headers are present
30
+ # or Array of Arrays if no headers
31
+ def to_a
32
+ if @has_headers
33
+ # Convert rows to hashes, which will automatically exclude values without headers
34
+ @rows.map(&:to_hash)
53
35
  else
54
- @rows.first&.values || []
36
+ # When no headers, return array of arrays with consistent length
37
+ max_length = @rows.map { |row| row.values.length }.max || 0
38
+ @rows.map do |row|
39
+ values = row.values
40
+ values + Array.new(max_length - values.length, '')
41
+ end
55
42
  end
56
43
  end
57
44
 
58
- def build_markdown_table(keys, column_widths)
59
- result = []
60
-
61
- # Add header row
62
- result << row_to_markdown(keys, column_widths)
63
-
64
- # Add separator row
65
- separator, _ = Row.separator_row(column_widths)
66
- result << separator
67
-
68
- # Add data rows
69
- rows_to_render.each do |row|
70
- result << row.to_markdown(column_widths)
71
- end
72
-
73
- result.join("\n")
45
+ def to_html
46
+ Formatters::Base.for(:html).format(@rows, @headers)
74
47
  end
75
48
 
76
- def rows_to_render
77
- if !@headers && !@rows.first&.data.is_a?(Hash) && @rows.size > 1
78
- @rows[1..-1]
79
- else
80
- @rows
81
- end
49
+ # Generate markdown representation
50
+ def to_md
51
+ Formatters::Base.for(:markdown).format(@rows, @headers)
82
52
  end
53
+ alias generate to_md
83
54
 
84
- def parse_content(markdown_table)
85
- # Split content into rows
86
- rows = markdown_table.split("\n").map(&:strip).reject(&:empty?)
87
- return if rows.empty?
88
-
89
- if @headers
90
- parse_with_headers(rows)
91
- else
92
- parse_without_headers(rows)
93
- end
55
+ # Generate CSV representation
56
+ def to_csv
57
+ Formatters::Base.for(:csv).format(@rows, @headers)
94
58
  end
95
59
 
96
- def parse_with_headers(rows)
97
- # Extract headers from first row
98
- header_values = Row.parse(rows.first)
99
- @header_row = header_values.each_with_object({}) { |val, hash| hash[val] = val }
100
-
101
- # Process each data row
102
- rows.each_with_index do |row, index|
103
- # Skip header row and separator rows
104
- next if index == 0 || Row.separator?(row)
105
-
106
- # Parse the row into values
107
- values = Row.parse(row)
108
-
109
- # Create a hash mapping headers to values
110
- row_hash = {}
111
- header_values.each_with_index do |header, i|
112
- row_hash[header] = i < values.length ? values[i] : ''
113
- end
114
-
115
- @rows << Row.new(row_hash, headers: header_values)
116
- end
60
+ # Support for accessing by index like table[0]
61
+ def [](index)
62
+ @rows[index]
117
63
  end
118
64
 
119
- def parse_without_headers(rows)
120
- # When headers: false, store array of arrays
121
- rows.each do |row|
122
- # Skip separator rows
123
- next if Row.separator?(row)
124
-
125
- # Parse the row into values
126
- values = Row.parse(row)
127
- @rows << Row.new(values, headers: nil)
128
- end
65
+ # Returns the number of rows
66
+ def size
67
+ @rows.size
129
68
  end
69
+ alias length size
130
70
 
131
- # Calculate the maximum width of each column
132
- def calculate_column_widths(arrays_of_values)
133
- max_column_count = arrays_of_values.map { |row| row.size }.max || 0
134
- column_widths = Array.new(max_column_count, 0)
135
-
136
- arrays_of_values.each do |row|
137
- row.each_with_index do |cell, i|
138
- cell_width = cell.to_s.length
139
- column_widths[i] = [column_widths[i], cell_width].max
140
- end
141
- end
142
-
143
- column_widths
71
+ def empty?
72
+ @rows.empty?
144
73
  end
145
74
 
146
- # Generate markdown row from array of values with proper spacing
147
- def row_to_markdown(values, column_widths)
148
- formatted_values = values.each_with_index.map do |val, i|
149
- val.to_s.ljust(column_widths[i])
150
- end
151
- "| #{formatted_values.join(' | ')} |"
75
+ private
76
+
77
+ def validate_headers
78
+ duplicates = @headers.group_by { |h| h }.select { |_, v| v.size > 1 }.keys
79
+ return unless duplicates.any?
80
+
81
+ raise ArgumentError, "Duplicate headers are not allowed: #{duplicates.join(', ')}"
152
82
  end
153
83
  end
154
84
  end