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 +7 -0
- data/LICENSE +21 -0
- data/README.md +34 -0
- data/lib/marktable/row.rb +98 -0
- data/lib/marktable/table.rb +154 -0
- data/lib/marktable.rb +136 -0
- metadata +60 -0
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: []
|