marktable 0.0.4s → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '09cd0e8dca57fcd46c94f64c80ccba04609368bac7e7ddd6daf7c6d3ffd67320'
4
- data.tar.gz: 90b6106afd8f92d4810fd786b58c925a21447738568afc3bcc344def6f6bcff6
3
+ metadata.gz: f4dc7dda731d7cbac0c2b155c3c572c44954ca2f65281f3cf976b05ba9cd1532
4
+ data.tar.gz: 1261f5a0afd0bbf45a16d8e9d369ef61b8e9313e83788a1b5e10be8871fac962
5
5
  SHA512:
6
- metadata.gz: 8fa4867f6bfd9b9d9dabf2087203d501f2149c381c91479f6d79f45f6431e2cfc3096078a8f1f06cdcff9bb0e4a397af64af6adbd16a9a6fd515cbbd9f2cec36
7
- data.tar.gz: 1bc26eb0ca767b832c90f636b8001a3c3967bd0369e4745a302301063ff8536251a9bbc879c1c88707afd6a19185b6442fd2370514ad8aaa371740e219811b4f
6
+ metadata.gz: 8127ab236b08a55f8d8d2fafcb116996d347527085af2dc1e71632b55930d326eae6e6b8bbac2e55c33965effa4d14863b4741e80c70297b2079bf715153dab6
7
+ data.tar.gz: 8400c3fbb70a9330d2db6c36f23599161ae35e4766c614597f773f38305ae9bf62708d053477d5e727d7cf51dee76490bb23743c0c77aca76a4a87997795e9be
data/README.md CHANGED
@@ -1,8 +1,27 @@
1
1
  # Marktable
2
2
 
3
- Marktable is a Ruby gem for ...
4
3
 
5
- ## Installation
4
+ A powerful Ruby library for parsing, manipulating, and generating Markdown tables with an elegant and intuitive API.
5
+
6
+ ## 📚 Overview
7
+
8
+ Marktable allows you to seamlessly work with Markdown tables using familiar Ruby data structures. Whether you're parsing tables from Markdown documents, generating tables for documentation, or manipulating tabular data, Marktable provides simple yet powerful tools for all your Markdown table needs.
9
+
10
+ ## ✨ Features
11
+
12
+ - **Custom RSpec matcher** for testing Markdown-like data structures (including html tables) against expected Markdown output
13
+ - **Parse** Markdown tables into Ruby data structures (arrays or hashes)
14
+ - **Generate** beautifully formatted Markdown tables from Ruby objects
15
+ - **Filter** and **transform** table data with Ruby's familiar block syntax
16
+ - **Auto-detect** headers from properly formatted Markdown tables
17
+ - **Handle** tables with or without headers
18
+ - **Support** for mismatched columns and keys in data
19
+ - **Convert** between array-based and hash-based representations
20
+
21
+ ## Not supported
22
+ - Non-string values (E.g. complex objects) in the table rows.
23
+
24
+ ## 📦 Installation
6
25
 
7
26
  Add this line to your application's Gemfile:
8
27
 
@@ -22,13 +41,333 @@ Or install it yourself as:
22
41
  gem install marktable
23
42
  ```
24
43
 
25
- ## Usage
44
+ ## 🚀 Quick Start
45
+
46
+ ### Main use case (Rspec only at this time)
47
+
48
+ This gem started as a pet project aiming at:
49
+ - Simplifying the process of testing html table content
50
+ - Making these specs more developer-friendly, with a very readable format
51
+
52
+
53
+ ```ruby
54
+ visit my_path
55
+ actual_table = page.find('#my-table')
56
+ expected_table = <<~MARKDOWN
57
+ | Name | Age | City |
58
+ | ----- | --- | -------- |
59
+ | John | 30 | New York |
60
+ | Jane | 25 | Boston |
61
+ | Bob | 17 | Chicago |
62
+ MARKDOWN
63
+
64
+ expect(actual_table).to match_markdown(expected_table)
65
+ ```
66
+
67
+ In case of semantical mismatch, the matcher will show an easy to verify markdown representation of the tables:
68
+
69
+ ```markdown
70
+ Expected markdown table to match:
71
+
72
+ Expected:
73
+ | Name | Age | City |
74
+ | ----- | --- | -------- |
75
+ | John | 30 | New York |
76
+ | Jane | 25 | Boston |
77
+ | Bob | 17 | Chicago |
78
+
79
+ Actual:
80
+ | Name | Age |
81
+ | ---- | --- |
82
+ | John | 31 |
83
+ | Jane | 25 |
84
+
85
+ Parsed expected data: [{"Name" => "John", "Age" => "30"}, {"Name" => "Jane", "Age" => "25"}, {"Name" => "Bob", "Age" => "17"}]
86
+ Parsed actual data: [{"Name" => "John", "Age" => "31"}, {"Name" => "Jane", "Age" => "25"}]
87
+ ```
88
+
89
+
90
+
91
+ ### Parsing a Markdown Table
26
92
 
27
93
  ```ruby
28
94
  require 'marktable'
29
- # Example usage here
95
+
96
+ markdown_table = <<~MARKDOWN
97
+ | Name | Age | City |
98
+ | ----- | --- | -------- |
99
+ | John | 30 | New York |
100
+ | Jane | 25 | Boston |
101
+ MARKDOWN
102
+
103
+ # Parse into an array of hashes (with auto-detected headers)
104
+ data = Marktable.parse(markdown_table)
105
+ # => [
106
+ # {"Name"=>"John", "Age"=>"30", "City"=>"New York"},
107
+ # {"Name"=>"Jane", "Age"=>"25", "City"=>"Boston"}
108
+ # ]
109
+
110
+ # Access data easily
111
+ puts data.first["Name"] # => "John"
112
+ ```
113
+
114
+ ### Generating a Markdown Table
115
+
116
+ ```ruby
117
+ # Create a new table and add rows
118
+ table = Marktable.generate do |t|
119
+ t << {"Name" => "John", "Age" => "30", "City" => "New York"}
120
+ t << {"Name" => "Jane", "Age" => "25", "City" => "Boston"}
121
+ t << {"Name" => "Bob", "Age" => "17", "City" => "Chicago"}
122
+ end
123
+
124
+ puts table
125
+ # | Name | Age | City |
126
+ # | ---- | --- | -------- |
127
+ # | John | 30 | New York |
128
+ # | Jane | 25 | Boston |
129
+ # | Bob | 17 | Chicago |
130
+ ```
131
+
132
+ ## 📖 Usage Guide
133
+
134
+ ### Working with Table Objects
135
+
136
+ Create a table object for more advanced operations:
137
+
138
+ ```ruby
139
+ # Create from a markdown string
140
+ table = Marktable.new(markdown_table)
141
+
142
+ # Or create from array data
143
+ array_data = [
144
+ ["Name", "Age", "City"],
145
+ ["John", "30", "New York"],
146
+ ["Jane", "25", "Boston"],
147
+ ["Bob", "17", "Chicago"]
148
+ ]
149
+ table = Marktable.new(array_data, headers: true)
150
+
151
+ # Or with auto-detected headers
152
+ array_data = [
153
+ { "Name" => "John", "Age" => "30", "City" => "New York" },
154
+ { "Name" => "Jane", "Age" => "25", "City" => "Boston" },
155
+ { "Name" => "Bob", "Age" => "17", "City" => "Chicago" }
156
+ ]
157
+ table = Marktable.new(array_data)
158
+ ```
159
+
160
+ ### Filtering Rows
161
+
162
+ Filter rows using pattern matching or blocks:
163
+
164
+ ```ruby
165
+ # Filter with a regex pattern
166
+ nyc_residents = table.filter(/New York/)
167
+
168
+ # Filter with a block
169
+ adults = table.filter do |row|
170
+ row["Age"].to_i >= 18
171
+ end
172
+
173
+ puts adults
174
+ # | Name | Age | City |
175
+ # | ---- | --- | -------- |
176
+ # | John | 30 | New York |
177
+ # | Jane | 25 | Boston |
178
+ ```
179
+
180
+ ### Transforming Data
181
+
182
+ Transform table data with the map method:
183
+
184
+ ```ruby
185
+ # Add 5 years to everyone's age
186
+ older = table.map do |row|
187
+ row.merge("Age" => (row["Age"].to_i + 5).to_s)
188
+ end
189
+
190
+ puts older
191
+ # | Name | Age | City |
192
+ # | ---- | --- | -------- |
193
+ # | John | 35 | New York |
194
+ # | Jane | 30 | Boston |
195
+ # | Bob | 22 | Chicago |
196
+ ```
197
+
198
+ ### Working with Arrays
199
+
200
+ Use arrays instead of hashes for row data:
201
+
202
+ ```ruby
203
+ # Create a table with array rows
204
+ table = Marktable.new([], headers: false)
205
+ table << ["John", "30", "New York"]
206
+ table << ["Jane", "25", "Boston"]
207
+
208
+ puts table
209
+ # | John | 30 | New York |
210
+ # | Jane | 25 | Boston |
211
+ ```
212
+
213
+ ### Mixed Row Types
214
+
215
+ Marktable can handle mixed row types (arrays and hashes):
216
+
217
+ ```ruby
218
+ table = Marktable.generate do |t|
219
+ t << {"Name" => "John", "Age" => "30", "City" => "New York"}
220
+ t << ["Jane", "25", "Boston"] # Array with same column count
221
+ end
222
+
223
+ puts table
224
+ # | Name | Age | City |
225
+ # | ---- | --- | -------- |
226
+ # | John | 30 | New York |
227
+ # | Jane | 25 | Boston |
228
+ ```
229
+
230
+ ## 🧪 Testing with Custom Matchers
231
+
232
+ Marktable includes a custom RSpec matcher, `match_markdown`, to make testing Markdown-compatible data structures simple and reliable.
233
+
234
+ ### Using the `match_markdown` Matcher
235
+
236
+ First, require the matcher in your spec_helper.rb or in the specific test file:
237
+
238
+ ```ruby
239
+ require 'marktable/rspec'
240
+ ```
241
+
242
+ #### Testing Markdown Table Output
243
+
244
+ ```ruby
245
+ RSpec.describe ReportGenerator do
246
+ describe "#generate_customer_table" do
247
+ it "generates the expected customer table" do
248
+ actual = ReportGenerator.new(customers).generate_table
249
+ # Presume actual contains:
250
+ # [
251
+ # { id: 1, name: "John Smith", status: "Active" },
252
+ # { id: 2, name: "Jane Doe", status: "Pending" }
253
+ # ]
254
+
255
+ expected = <<~MARKDOWN
256
+ | ID | Name | Status |
257
+ | -- | ---------- | ------- |
258
+ | 1 | John Smith | Active |
259
+ | 2 | Jane Doe | Pending |
260
+ MARKDOWN
261
+
262
+ expect(result).to match_markdown(expected)
263
+ end
264
+ end
265
+ end
30
266
  ```
31
267
 
32
- ## Development
268
+ #### Testing HTML Table Output
269
+
270
+ The `match_markdown` matcher can also compare HTML tables by extracting their semantic content:
271
+
272
+ ```ruby
273
+ RSpec.describe HtmlReportGenerator do
274
+ describe "#generate_sales_report" do
275
+ it "generates the expected HTML table" do
276
+ sales_data = [
277
+ { product: "Widget A", quantity: 150, revenue: "$3,000" },
278
+ { product: "Widget B", quantity: 75, revenue: "$1,875" }
279
+ ]
280
+
281
+ generator = HtmlReportGenerator.new(sales_data)
282
+ html_output = generator.generate_sales_report
283
+
284
+ # The matcher extracts the data structure from both the HTML and expected markdown
285
+ expected = <<~MARKDOWN
286
+ | Product | Quantity | Revenue |
287
+ | -------- | -------- | ------- |
288
+ | Widget A | 150 | $3,000 |
289
+ | Widget B | 75 | $1,875 |
290
+ MARKDOWN
291
+
292
+ # This will pass even though one is HTML and one is Markdown!
293
+ expect(html_output).to match_markdown(expected)
294
+ end
295
+ end
296
+ end
297
+ ```
298
+
299
+ #### Testing API JSON Responses
300
+
301
+ The matcher is also helpful when testing APIs that return tabular data:
302
+
303
+ ```ruby
304
+ RSpec.describe "Products API" do
305
+ describe "GET /api/products" do
306
+ it "returns products in the expected format" do
307
+ # Setup test data and make request
308
+ get "/api/products"
309
+
310
+ # Parse JSON response
311
+ json_response = JSON.parse(response.body)
312
+
313
+ # Convert API response to a table
314
+ table = Marktable.new(json_response)
315
+
316
+ expected = <<~MARKDOWN
317
+ | id | name | category | price |
318
+ | -- | ----------- | -------- | ------ |
319
+ | 1 | Product One | Books | $19.99 |
320
+ | 2 | Product Two | Games | $59.99 |
321
+ MARKDOWN
322
+
323
+ expect(table).to match_markdown(expected)
324
+ end
325
+ end
326
+ end
327
+ ```
328
+
329
+ The `match_markdown` matcher compares tables semantically rather than character-by-character, which means:
330
+
331
+ - It ignores differences in whitespace padding
332
+ - It handles different column ordering in hash-based tables
333
+ - It works with both HTML and Markdown table formats
334
+ - It provides clear error messages showing the differences between tables
335
+
336
+ ## 📋 API Reference
337
+
338
+ ### Class Methods
339
+
340
+ - `Marktable.parse(table, headers: nil)` - Parse table string or array into an array of hashes/arrays
341
+ - `Marktable.new(table, headers: nil)` - Create a new Table object
342
+ - `Marktable.parse_line(row)` - Parse a single markdown row into an array
343
+ - `Marktable.generate { |t| ... }` - Generate a table with a block
344
+
345
+ ### Instance Methods
346
+
347
+ - `table.to_a` - Convert table to array of hashes/arrays
348
+ - `table.to_s` / `table.generate` - Generate markdown representation
349
+ - `table.empty?` - Check if table is empty
350
+ - `table.column_count` - Get column count
351
+ - `table << row_data` - Add a row to the table
352
+ - `table.filter(pattern = nil) { |row| ... }` - Filter rows by pattern or block
353
+ - `table.map { |row| ... }` - Transform rows with a block
354
+
355
+ ## 🧪 Development
356
+
357
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
358
+
359
+ To install this gem onto your local machine, run `bundle exec rake install`.
360
+
361
+ ## 🤝 Contributing
362
+
363
+ Bug reports and pull requests are welcome on GitHub at https://github.com/yourusername/marktable. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/yourusername/marktable/blob/main/CODE_OF_CONDUCT.md).
364
+
365
+ 1. Fork the repository
366
+ 2. Create your feature branch: `git checkout -b my-new-feature`
367
+ 3. Commit your changes: `git commit -am 'Add some feature'`
368
+ 4. Push to the branch: `git push origin my-new-feature`
369
+ 5. Submit a pull request
370
+
371
+ ## 📄 License
33
372
 
34
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
373
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -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.each_value 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