├── .gitignore ├── Gemfile ├── README.md ├── Rakefile ├── bin └── csv2md ├── csv2md.gemspec ├── lib └── csv2md.rb └── test ├── fixtures ├── malformed.csv ├── sample.csv └── sample.md ├── lib └── csv2md_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem's dependencies in ..gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSV to GitHub Flavored Markdown Table Converter 2 | 3 | Convert [csv formatted data](http://en.wikipedia.org/wiki/Comma-separated_values) to a [GitHub Flavored Markdown table](https://help.github.com/articles/github-flavored-markdown/#tables). 4 | 5 | ## Installation 6 | 7 | ```bash 8 | $ gem install csv2md 9 | Successfully installed csv2md-1.0.0 10 | 1 gem installed 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```bash 16 | $ csv2md 17 | Usage: csv2md [options] results.csv 18 | cat results.csv | csv2md [options] 19 | 20 | Options: 21 | -r Reverses conversion, takes a GitHub Flavored 22 | Markdown table as input and outputs csv. 23 | 24 | $ cat foo.csv 25 | Name,Title,Email Address 26 | Jane Atler,CEO,jane@acme.com 27 | John Doherty,CTO,john@acme.com 28 | Sally Smith,CFO,sally@acme.com 29 | 30 | $ csv2md foo.csv 31 | | Name | Title | Email Address | 32 | |--------------|-------|----------------| 33 | | Jane Atler | CEO | jane@acme.com | 34 | | John Doherty | CTO | john@acme.com | 35 | | Sally Smith | CFO | sally@acme.com | 36 | 37 | $ cat foo.csv | csv2md 38 | | Name | Title | Email Address | 39 | |--------------|-------|----------------| 40 | | Jane Atler | CEO | jane@acme.com | 41 | | John Doherty | CTO | john@acme.com | 42 | | Sally Smith | CFO | sally@acme.com | 43 | ``` 44 | 45 | It supports one option right now, `-r`: 46 | 47 | ```bash 48 | $ cat foo.csv | csv2md | csv2md -r 49 | Name,Title,Email Address 50 | Jane Atler,CEO,jane@acme.com 51 | John Doherty,CTO,john@acme.com 52 | Sally Smith,CFO,sally@acme.com 53 | ``` 54 | 55 | ## Contributors 56 | 57 | * [Jonathan Hoyt](https://github.com/jonmagic) 58 | * [Jiri Nemecek](https://github.com/geronime) 59 | * [Will Fitzgerald](https://github.com/willf) 60 | 61 | ## License 62 | 63 | The MIT License (MIT) 64 | 65 | Copyright (c) 2015 Jonathan Hoyt 66 | 67 | Permission is hereby granted, free of charge, to any person obtaining a copy 68 | of this software and associated documentation files (the "Software"), to deal 69 | in the Software without restriction, including without limitation the rights 70 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 71 | copies of the Software, and to permit persons to whom the Software is 72 | furnished to do so, subject to the following conditions: 73 | 74 | The above copyright notice and this permission notice shall be included in all 75 | copies or substantial portions of the Software. 76 | 77 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 78 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 79 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 80 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 81 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 82 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 83 | SOFTWARE. 84 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/testtask" 2 | 3 | Rake::TestTask.new(:test) do |t| 4 | t.libs << "test" 5 | t.test_files = FileList["test/**/*_test.rb"] 6 | end 7 | 8 | task :default => :test 9 | -------------------------------------------------------------------------------- /bin/csv2md: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "csv2md" 4 | 5 | input = (STDIN.tty?) ? nil : $stdin.read 6 | gfm = "" 7 | 8 | if !input && ARGV.length == 0 9 | message = "Usage: csv2md [options] results.csv\n" 10 | message += " cat results.csv | csv2md [options]\n\n" 11 | message += "Options:\n" 12 | message += " -r Reverses conversion, takes a GitHub Flavored\n" 13 | message += " Markdown table as input and outputs csv." 14 | puts message 15 | exit 16 | end 17 | 18 | begin 19 | output_format = if ARGV[0] == "-r" 20 | ARGV.shift 21 | :csv 22 | else 23 | :gfm 24 | end 25 | 26 | if input 27 | csv2md = Csv2md.new(input) 28 | gfm = csv2md.send(output_format) 29 | else 30 | while file_path = ARGV.shift 31 | input = File.read(file_path) 32 | csv2md = Csv2md.new(input) 33 | gfm += csv2md.send(output_format) 34 | gfm += "\n" if ARGV.length > 0 35 | end 36 | end 37 | rescue Csv2md::UnableToParseCsv 38 | puts "Unable to parse csv" 39 | exit 40 | end 41 | 42 | puts gfm 43 | -------------------------------------------------------------------------------- /csv2md.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | spec.name = "csv2md" 3 | spec.version = "1.1.3" 4 | spec.date = "2015-04-29" 5 | spec.summary = "Convert csv into a GitHub Flavored Markdown table" 6 | spec.description = "Convert csv into a GitHub Flavored Markdown table" 7 | spec.authors = ["Jonathan Hoyt"] 8 | spec.email = "jonmagic@gmail.com" 9 | spec.files = ["lib/csv2md.rb"] 10 | spec.homepage = "https://github.com/jonmagic/csv2md" 11 | spec.license = "MIT" 12 | spec.executables << "csv2md" 13 | 14 | spec.add_development_dependency "rake", "~> 10.4" 15 | spec.add_development_dependency "minitest", "~> 5.6" 16 | end 17 | -------------------------------------------------------------------------------- /lib/csv2md.rb: -------------------------------------------------------------------------------- 1 | require "csv" 2 | 3 | class Csv2md 4 | class UnableToParseCsv < StandardError; end 5 | 6 | def initialize(input) 7 | @input = input 8 | end 9 | 10 | attr_reader :input 11 | 12 | def find_column_widths 13 | parsed_csv.inject(Array.new(parsed_csv[0].length, 0)) do |result, line| 14 | line.each_with_index do |column, i| 15 | if column.to_s.length > result[i] 16 | result[i] = column.length 17 | end 18 | end 19 | 20 | result 21 | end 22 | rescue 23 | raise UnableToParseCsv 24 | end 25 | 26 | def gfm 27 | result = "" 28 | widths = find_column_widths 29 | number_of_columns = widths.length 30 | 31 | parsed_csv.each_with_index do |line, row_index| 32 | line.each_with_index do |column, column_index| 33 | result += "| " 34 | result += column.to_s.ljust(widths[column_index] + 1, " ") 35 | if column_index == number_of_columns - 1 36 | result += "|\n" 37 | end 38 | end 39 | if row_index == 0 40 | widths.each do |width| 41 | result += "|".ljust(width + 3, "-") 42 | end 43 | result += "|\n" 44 | end 45 | end 46 | 47 | result 48 | end 49 | 50 | def csv 51 | result = input.split("\n").map do |line| 52 | row = line.scan(/\|([^\|]*)\s/).flatten.map(&:strip).join(",") 53 | row unless row.strip == "" 54 | end.compact.join("\n") 55 | result += "\n" 56 | result 57 | end 58 | 59 | private 60 | 61 | def parsed_csv 62 | @parsed_csv ||= CSV.parse(input) 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /test/fixtures/malformed.csv: -------------------------------------------------------------------------------- 1 | Name,Title,Email Address 2 | Jane Atler,CEO,jane@acme.com, 3 | John Doherty,CTO,john@acme.com 4 | Sally Smith,CFO,sally@acme.com,, 5 | -------------------------------------------------------------------------------- /test/fixtures/sample.csv: -------------------------------------------------------------------------------- 1 | Name,Title,Email Address 2 | Jane Atler,CEO,jane@acme.com 3 | John Doherty,CTO,john@acme.com 4 | Sally Smith,CFO,sally@acme.com 5 | -------------------------------------------------------------------------------- /test/fixtures/sample.md: -------------------------------------------------------------------------------- 1 | | Name | Title | Email Address | 2 | |--------------|-------|----------------| 3 | | Jane Atler | CEO | jane@acme.com | 4 | | John Doherty | CTO | john@acme.com | 5 | | Sally Smith | CFO | sally@acme.com | 6 | -------------------------------------------------------------------------------- /test/lib/csv2md_test.rb: -------------------------------------------------------------------------------- 1 | require_relative "../test_helper" 2 | 3 | class Csv2mdTest < Minitest::Test 4 | def test_find_column_widths_returns_array_of_column_widths 5 | csv = File.read(fixture_path_helper("sample.csv")) 6 | subject = Csv2md.new(csv) 7 | assert_equal [12, 5, 14], subject.find_column_widths 8 | end 9 | 10 | def test_find_column_widths_raises_error_with_malformed_data 11 | csv = File.read(fixture_path_helper("malformed.csv")) 12 | subject = Csv2md.new(csv) 13 | assert_raises(Csv2md::UnableToParseCsv) do 14 | subject.find_column_widths 15 | end 16 | end 17 | 18 | def test_gfm_returns_csv_converted_to_pretty_gfm_table 19 | csv = File.read(fixture_path_helper("sample.csv")) 20 | expected = File.read(fixture_path_helper("sample.md")) 21 | subject = Csv2md.new(csv) 22 | assert_equal expected, subject.gfm 23 | end 24 | 25 | def test_csv_returns_gfm_table_converted_to_csv 26 | md = File.read(fixture_path_helper("sample.md")) 27 | expected = File.read(fixture_path_helper("sample.csv")) 28 | subject = Csv2md.new(md) 29 | assert_equal expected, subject.csv 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 2 | require "csv2md" 3 | require "minitest/autorun" 4 | require "pathname" 5 | 6 | def path_helper(path_relative_to_project_root) 7 | project_root = File.expand_path("../", File.dirname(__FILE__)) 8 | Pathname(project_root).join(path_relative_to_project_root) 9 | end 10 | 11 | def fixture_path_helper(filename) 12 | path_helper("test/fixtures/#{filename}") 13 | end 14 | --------------------------------------------------------------------------------