marktable 0.0.5 → 0.1.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.
data/lib/marktable.rb CHANGED
@@ -1,140 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'marktable/version'
3
4
  require_relative 'marktable/row'
5
+ require_relative 'marktable/tables/base'
4
6
  require_relative 'marktable/table'
5
7
 
6
- if defined?(RSpec)
7
- require_relative '../spec/support/matchers/markdown_matchers'
8
- end
9
-
10
8
  module Marktable
11
- # Parse a markdown table string into an array of rows
12
- def self.parse(markdown_table, headers: true)
13
- Table.new(markdown_table, headers: headers).to_a
14
- end
15
-
16
- # Parse a single markdown row into an array of cell values
17
- def self.parse_line(markdown_row)
18
- Row.parse(markdown_row)
19
- end
20
-
21
- # Iterate through each row of a markdown table
22
- def self.foreach(markdown_table, headers: true)
23
- table = Table.new(markdown_table, headers: headers)
24
- return Enumerator.new do |yielder|
25
- table.each do |row|
26
- yielder << row.data
27
- end
28
- end unless block_given?
29
-
30
- table.each do |row|
31
- yield row.data
32
- end
33
- end
34
-
35
- # Generate a markdown table from provided data
36
- def self.generate(headers: nil)
37
- result = []
38
- markdown_table = ''
39
-
40
- if block_given?
41
- table_data = []
42
- yield table_data
43
-
44
- unless table_data.empty?
45
- # Ensure all data is stringified
46
- string_data = table_data.map do |row|
47
- if row.is_a?(Hash)
48
- row.transform_values(&:to_s)
49
- else
50
- row.map(&:to_s)
51
- end
52
- end
53
-
54
- # Create a Table object
55
- table = table(string_data, headers: headers.nil? ? true : headers)
56
-
57
- markdown_table = table.generate
58
- end
59
- end
60
-
61
- markdown_table
62
- end
63
-
64
- # Read a markdown table from a file
65
- def self.read(path, headers: true)
66
- content = File.read(path)
67
- Table.new(content, headers: headers)
68
- end
69
-
70
- # Write a markdown table to a file
71
- def self.write(path, table_or_data)
72
- content = if table_or_data.is_a?(Table)
73
- table_or_data.to_s
74
- else
75
- table(table_or_data).to_s
76
- end
77
-
78
- File.write(path, content)
9
+ def self.from_markdown(table, headers: nil)
10
+ Table.new(table, type: :markdown, headers:)
79
11
  end
80
12
 
81
- # Convert an array to a Marktable::Table
82
- def self.table(array, headers: true)
83
- table = Table.new([], headers: headers)
84
-
85
- # Ensure all data values are strings
86
- string_array = array.map do |row|
87
- # Handle Row instances by extracting their data
88
- if row.is_a?(Row)
89
- row.data
90
- elsif row.is_a?(Hash)
91
- row.transform_values(&:to_s)
92
- else
93
- row.map(&:to_s)
94
- end
95
- end
96
-
97
- if headers && string_array.first.is_a?(Hash)
98
- header_keys = string_array.first.keys
99
- table.instance_variable_set(:@header_row, header_keys.each_with_object({}) { |k, h| h[k] = k })
100
- table.instance_variable_set(:@rows, string_array.map { |row_data| Row.new(row_data, headers: header_keys) })
101
- else
102
- table.instance_variable_set(:@rows, string_array.map { |row_data| Row.new(row_data, headers: nil) })
103
- end
104
-
105
- table
13
+ def self.from_csv(table, headers: nil)
14
+ Table.new(table, type: :csv, headers:)
106
15
  end
107
16
 
108
- # Filter rows matching a pattern
109
- def self.filter(markdown_table, pattern, headers: true)
110
- table = Table.new(markdown_table, headers: headers)
111
- filtered_rows = table.to_a.select do |row|
112
- if row.is_a?(Hash)
113
- row.values.any? { |v| v.to_s.match?(pattern) }
114
- else
115
- row.any? { |v| v.to_s.match?(pattern) }
116
- end
117
- end
118
-
119
- table(filtered_rows, headers: headers)
17
+ def self.from_array(table, headers: nil)
18
+ Table.new(table, type: :array, headers:)
120
19
  end
121
20
 
122
- # Map over rows (all values will be converted to strings)
123
- def self.map(markdown_table, headers: true)
124
- table = Table.new(markdown_table, headers: headers)
125
- mapped_rows = []
126
-
127
- table.each do |row|
128
- result = yield(row)
129
- # Ensure result is string-compatible
130
- if result.is_a?(Hash)
131
- result = result.transform_values(&:to_s)
132
- elsif result.is_a?(Array)
133
- result = result.map(&:to_s)
134
- end
135
- mapped_rows << result
136
- end
137
-
138
- table(mapped_rows, headers: headers)
21
+ def self.from_html(table)
22
+ Table.new(table, type: :html)
139
23
  end
140
24
  end
@@ -4,152 +4,54 @@ require 'capybara'
4
4
  require 'nokogiri'
5
5
 
6
6
  RSpec::Matchers.define :match_markdown do |expected_markdown|
7
- match do |actual|
8
- @actual_data = parse_input(actual)
9
- @expected_data = parse_input(expected_markdown)
10
-
11
- normalize(@actual_data) == normalize(@expected_data)
7
+ chain :with_format do |format|
8
+ @format = format
12
9
  end
13
10
 
14
- failure_message do |actual|
15
- @actual_data = parse_input(actual)
16
- @expected_data = parse_input(expected_markdown)
11
+ match do |actual|
12
+ @expected_data = parse_input(expected_markdown, :markdown)
13
+ @format ||= infer_format(actual)
14
+ @actual_data = parse_input(actual, @format)
15
+
16
+ # Compare data using to_a for consistent comparison
17
+ @actual_data.to_a == @expected_data.to_a
18
+ end
17
19
 
20
+ failure_message do
18
21
  format_failure_message(@expected_data, @actual_data)
19
22
  end
20
23
 
21
- failure_message_when_negated do |actual|
22
- @actual_data = parse_input(actual)
23
-
24
+ failure_message_when_negated do
24
25
  "Expected markdown tables to differ, but they match:\n\n" \
25
- "#{format_as_markdown(@actual_data)}"
26
+ "#{@actual_data.to_md}"
26
27
  end
27
-
28
+
28
29
  private
29
-
30
- # Parse different types of inputs into a common data structure
31
- def parse_input(input)
32
- case input
33
- when String
34
- if looks_like_html?(input)
35
- parse_html_table(input)
36
- else
37
- Marktable.parse(input)
38
- end
30
+
31
+ def parse_input(input, format = nil)
32
+ return input if input.is_a?(Marktable::Table)
33
+
34
+ Marktable::Table.new(input, type: format)
35
+ end
36
+
37
+ def infer_format(data)
38
+ case data
39
+ when Array
40
+ :array
41
+ when CSV::Table
42
+ :csv
39
43
  when Marktable::Table
40
- input.to_a
41
- when Capybara::Node::Element
42
- parse_capybara_element(input)
44
+ :markdown
43
45
  else
44
- input
45
- end
46
- end
47
-
48
- def looks_like_html?(text)
49
- text.include?('<table') || text.include?('<tr') || text.include?('<td')
50
- end
51
-
52
- # Normalize data by trimming whitespace in cell values
53
- def normalize(data)
54
- data.map do |row|
55
- if row.is_a?(Hash)
56
- row.transform_values { |v| v.to_s.strip }
57
- else
58
- row.map { |v| v.to_s.strip }
59
- end
46
+ :markdown
60
47
  end
61
48
  end
62
-
49
+
63
50
  def format_failure_message(expected_data, actual_data)
64
- expected_formatted = format_as_markdown(expected_data)
65
- actual_formatted = format_as_markdown(actual_data)
66
-
67
51
  "Expected markdown table to match:\n\n" \
68
- "Expected:\n#{expected_formatted}\n\n" \
69
- "Actual:\n#{actual_formatted}\n\n" \
70
- "Parsed expected data: #{expected_data.inspect}\n" \
71
- "Parsed actual data: #{actual_data.inspect}"
72
- end
73
-
74
- def format_as_markdown(data)
75
- Marktable.table(data).to_s
76
- end
77
-
78
- # Parse HTML table into rows of data using Nokogiri
79
- def parse_html_table(html)
80
- doc = Nokogiri::HTML(html)
81
-
82
- # Extract headers
83
- headers = extract_headers_with_nokogiri(doc)
84
-
85
- # Extract body rows
86
- body_rows = extract_body_rows_with_nokogiri(doc)
87
-
88
- # Convert rows to hashes using the headers
89
- body_rows.map do |row|
90
- row_to_hash(row, headers)
91
- end
92
- end
93
-
94
- def extract_headers_with_nokogiri(doc)
95
- headers = doc.css('thead th, thead td').map(&:text)
96
- if headers.empty? && doc.css('tr').any?
97
- headers = doc.css('tr:first-child th, tr:first-child td').map(&:text)
98
- end
99
- headers
100
- end
101
-
102
- def extract_body_rows_with_nokogiri(doc)
103
- tbody_rows = doc.css('tbody tr').map { |tr| tr.css('th, td').map(&:text) }
104
-
105
- # If no tbody, use all rows after the first (assuming first is header)
106
- if tbody_rows.empty?
107
- tbody_rows = doc.css('tr')[1..-1].to_a.map { |tr| tr.css('th, td').map(&:text) }
108
- end
109
-
110
- tbody_rows
111
- end
112
-
113
- def row_to_hash(cells, headers)
114
- row_hash = {}
115
- headers.each_with_index do |header, i|
116
- row_hash[header] = i < cells.length ? cells[i] : ''
117
- end
118
- row_hash
119
- end
120
-
121
- def parse_capybara_element(element)
122
- # Extract headers
123
- headers = extract_headers_from_capybara(element)
124
-
125
- # Extract body rows
126
- body_rows = extract_body_rows_from_capybara(element)
127
-
128
- # Convert rows to hashes using the headers
129
- body_rows.map do |cells|
130
- row_to_hash(cells, headers)
131
- end
132
- end
133
-
134
- def extract_headers_from_capybara(element)
135
- thead = element.first('thead') rescue nil
136
- if thead
137
- thead.all('th, td').map(&:text)
138
- else
139
- first_row = element.first('tr')
140
- first_row ? first_row.all('th, td').map(&:text) : []
141
- end
142
- end
143
-
144
- def extract_body_rows_from_capybara(element)
145
- body_rows = element.all('tbody tr')
146
-
147
- # If no tbody, assume first row is header and skip it
148
- if body_rows.empty?
149
- all_rows = element.all('tr')
150
- body_rows = all_rows[1..]
151
- end
152
-
153
- body_rows.map { |tr| tr.all('th, td').map(&:text) }
52
+ "Expected:\n#{expected_data.to_md}\n\n" \
53
+ "Actual:\n#{actual_data.to_md}\n\n" \
54
+ "Parsed expected data: #{expected_data.to_a.inspect}\n" \
55
+ "Parsed actual data: #{actual_data.to_a.inspect}"
154
56
  end
155
57
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: marktable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Francois Gaspard
@@ -10,19 +10,19 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: rake
13
+ name: csv
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '13.0'
19
- type: :development
18
+ version: '3.0'
19
+ type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '13.0'
25
+ version: '3.0'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: nokogiri
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '1.14'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rake
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '13.0'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '13.0'
40
54
  description: Provides a row-based object model and utility methods for creating, parsing,
41
55
  transforming, and exporting Markdown tables in Ruby.
42
56
  email:
@@ -48,8 +62,18 @@ files:
48
62
  - LICENSE
49
63
  - README.md
50
64
  - lib/marktable.rb
65
+ - lib/marktable/formatters/base.rb
66
+ - lib/marktable/formatters/csv.rb
67
+ - lib/marktable/formatters/html.rb
68
+ - lib/marktable/formatters/markdown.rb
51
69
  - lib/marktable/row.rb
52
70
  - lib/marktable/table.rb
71
+ - lib/marktable/tables/array.rb
72
+ - lib/marktable/tables/base.rb
73
+ - lib/marktable/tables/csv.rb
74
+ - lib/marktable/tables/html.rb
75
+ - lib/marktable/tables/markdown.rb
76
+ - lib/marktable/version.rb
53
77
  - spec/support/matchers/markdown_matchers.rb
54
78
  homepage: https://github.com/Francois-gaspard/marktable
55
79
  licenses: