marktable 0.0.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 662b8c0026118b8a9a8cbfcefc4e22ca2d245020672b04e9dc17e923356b60cd
4
+ data.tar.gz: 1307c073aeea515d89ee1e40c5d3a6753f6de815c7ae2f72f1ed042c9a733436
5
+ SHA512:
6
+ metadata.gz: 8fe53f45c22f224efece661fcdbcd4fbd61426211f331d592e93dfa33953344b64e73b1d8558d5640f01fd13d67f71f80fc63c42e55b7212f6326da7497911b4
7
+ data.tar.gz: e50d5d8cc11159dd809adb47b4f44a71116dab221a70b1c7a73495f7c4377601a6f7a59dc1afef9b67e7943758a86adaa1006460f02f4fe0f55fe9bd5387237d
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Your Name
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # Marktable
2
+
3
+ Marktable is a Ruby gem for ...
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'marktable'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```bash
16
+ bundle install
17
+ ```
18
+
19
+ Or install it yourself as:
20
+
21
+ ```bash
22
+ gem install marktable
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```ruby
28
+ require 'marktable'
29
+ # Example usage here
30
+ ```
31
+
32
+ ## Development
33
+
34
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests.
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Marktable
4
+ class Row
5
+ attr_reader :data, :headers
6
+
7
+ def initialize(data = {}, headers: nil)
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
22
+ else
23
+ # Keep as array when no headers
24
+ data_strings
25
+ end
26
+ else
27
+ @data = headers ? {} : []
28
+ end
29
+ end
30
+
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
38
+ end
39
+ end
40
+
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
46
+ end
47
+ end
48
+
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
64
+ end
65
+
66
+ def to_a
67
+ @data.is_a?(Array) ? @data : @data.values
68
+ end
69
+
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)
75
+ end
76
+ "| #{formatted_values.join(' | ')} |"
77
+ end
78
+
79
+ # Parse a markdown row string into an array of values
80
+ def self.parse(row_string)
81
+ row_string.strip.sub(/^\|/, '').sub(/\|$/, '').split('|').map(&:strip)
82
+ end
83
+
84
+ # Check if a row string represents a separator row
85
+ def self.separator?(row_string)
86
+ row_string.strip.gsub(/[\|\-\s]/, '').empty?
87
+ end
88
+
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
93
+ end
94
+
95
+ ["| #{separators.join(' | ')} |", column_widths]
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Marktable
4
+ class Table
5
+ include Enumerable
6
+
7
+ attr_reader :headers
8
+
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?
14
+ end
15
+
16
+ def each
17
+ if block_given?
18
+ @rows.each { |row| yield(row) }
19
+ else
20
+ @rows.each
21
+ end
22
+ end
23
+
24
+ def to_a
25
+ @rows.map { |row| row.data }
26
+ end
27
+
28
+ def to_s
29
+ generate
30
+ end
31
+
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
53
+ else
54
+ @rows.first&.values || []
55
+ end
56
+ end
57
+
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")
74
+ end
75
+
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
82
+ end
83
+
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
94
+ end
95
+
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
117
+ end
118
+
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
129
+ end
130
+
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
144
+ end
145
+
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(' | ')} |"
152
+ end
153
+ end
154
+ end
data/lib/marktable.rb ADDED
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'marktable/row'
4
+ require_relative 'marktable/table'
5
+
6
+ module Marktable
7
+ # Parse a markdown table string into an array of rows
8
+ def self.parse(markdown_table, headers: true)
9
+ Table.new(markdown_table, headers: headers).to_a
10
+ end
11
+
12
+ # Parse a single markdown row into an array of cell values
13
+ def self.parse_line(markdown_row)
14
+ Row.parse(markdown_row)
15
+ end
16
+
17
+ # Iterate through each row of a markdown table
18
+ def self.foreach(markdown_table, headers: true)
19
+ table = Table.new(markdown_table, headers: headers)
20
+ return Enumerator.new do |yielder|
21
+ table.each do |row|
22
+ yielder << row.data
23
+ end
24
+ end unless block_given?
25
+
26
+ table.each do |row|
27
+ yield row.data
28
+ end
29
+ end
30
+
31
+ # Generate a markdown table from provided data
32
+ def self.generate(headers: nil)
33
+ result = []
34
+ markdown_table = ''
35
+
36
+ if block_given?
37
+ table_data = []
38
+ yield table_data
39
+
40
+ unless table_data.empty?
41
+ # Ensure all data is stringified
42
+ string_data = table_data.map do |row|
43
+ if row.is_a?(Hash)
44
+ row.transform_values(&:to_s)
45
+ else
46
+ row.map(&:to_s)
47
+ end
48
+ end
49
+
50
+ # Create a Table object
51
+ table = table(string_data, headers: headers.nil? ? true : headers)
52
+
53
+ markdown_table = table.generate
54
+ end
55
+ end
56
+
57
+ markdown_table
58
+ end
59
+
60
+ # Read a markdown table from a file
61
+ def self.read(path, headers: true)
62
+ content = File.read(path)
63
+ Table.new(content, headers: headers)
64
+ end
65
+
66
+ # Write a markdown table to a file
67
+ def self.write(path, table_or_data)
68
+ content = if table_or_data.is_a?(Table)
69
+ table_or_data.to_s
70
+ else
71
+ table(table_or_data).to_s
72
+ end
73
+
74
+ File.write(path, content)
75
+ end
76
+
77
+ # Convert an array to a Marktable::Table
78
+ def self.table(array, headers: true)
79
+ table = Table.new([], headers: headers)
80
+
81
+ # Ensure all data values are strings
82
+ string_array = array.map do |row|
83
+ # Handle Row instances by extracting their data
84
+ if row.is_a?(Row)
85
+ row.data
86
+ elsif row.is_a?(Hash)
87
+ row.transform_values(&:to_s)
88
+ else
89
+ row.map(&:to_s)
90
+ end
91
+ end
92
+
93
+ if headers && string_array.first.is_a?(Hash)
94
+ header_keys = string_array.first.keys
95
+ table.instance_variable_set(:@header_row, header_keys.each_with_object({}) { |k, h| h[k] = k })
96
+ table.instance_variable_set(:@rows, string_array.map { |row_data| Row.new(row_data, headers: header_keys) })
97
+ else
98
+ table.instance_variable_set(:@rows, string_array.map { |row_data| Row.new(row_data, headers: nil) })
99
+ end
100
+
101
+ table
102
+ end
103
+
104
+ # Filter rows matching a pattern
105
+ def self.filter(markdown_table, pattern, headers: true)
106
+ table = Table.new(markdown_table, headers: headers)
107
+ filtered_rows = table.to_a.select do |row|
108
+ if row.is_a?(Hash)
109
+ row.values.any? { |v| v.to_s.match?(pattern) }
110
+ else
111
+ row.any? { |v| v.to_s.match?(pattern) }
112
+ end
113
+ end
114
+
115
+ table(filtered_rows, headers: headers)
116
+ end
117
+
118
+ # Map over rows (all values will be converted to strings)
119
+ def self.map(markdown_table, headers: true)
120
+ table = Table.new(markdown_table, headers: headers)
121
+ mapped_rows = []
122
+
123
+ table.each do |row|
124
+ result = yield(row)
125
+ # Ensure result is string-compatible
126
+ if result.is_a?(Hash)
127
+ result = result.transform_values(&:to_s)
128
+ elsif result.is_a?(Array)
129
+ result = result.map(&:to_s)
130
+ end
131
+ mapped_rows << result
132
+ end
133
+
134
+ table(mapped_rows, headers: headers)
135
+ end
136
+ end
metadata ADDED
@@ -0,0 +1,60 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: marktable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Francois Gaspard
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rake
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '13.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '13.0'
26
+ description: Provides a row-based object model and utility methods for creating, parsing,
27
+ transforming, and exporting Markdown tables in Ruby.
28
+ email:
29
+ - fr@ncois.email
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - LICENSE
35
+ - README.md
36
+ - lib/marktable.rb
37
+ - lib/marktable/row.rb
38
+ - lib/marktable/table.rb
39
+ homepage: https://github.com/Francois-gaspard/marktable
40
+ licenses:
41
+ - MIT
42
+ metadata: {}
43
+ rdoc_options: []
44
+ require_paths:
45
+ - lib
46
+ required_ruby_version: !ruby/object:Gem::Requirement
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ version: '3.4'
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements: []
57
+ rubygems_version: 3.6.7
58
+ specification_version: 4
59
+ summary: Read, write, parse, and filter Markdown tables easily.
60
+ test_files: []