├── .travis.yml ├── LICENSE ├── README.md ├── cmd └── mdt │ ├── main.go │ └── mdt ├── column.go ├── mdt.go ├── mdt_test.go ├── row.go ├── rows.go └── table.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.4.2 4 | - 1.4 5 | - 1.3 6 | - release 7 | - tip 8 | script: 9 | - go test -v ./... 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) [2015] [mdt] 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mdt [![Build Status](https://travis-ci.org/monochromegane/mdt.svg?branch=master)](https://travis-ci.org/monochromegane/mdt) 2 | 3 | A markdown table generation tool from CSV/TSV. 4 | 5 | ## Usage 6 | 7 | ```sh 8 | $ mdt < hoge.csv 9 | | headerA | headerB | 10 | | ------- | ---------------------- | 11 | | short | very very long content | 12 | ``` 13 | 14 | `mdt` is very simple command line tool, so you can integrate with other tools like `pbpaste`, `Automator`, etc... 15 | 16 | For example, you can use `mdt` with `Automator Service` on GitHub PR/Issue page. 17 | 18 | ![mdt](https://cloud.githubusercontent.com/assets/1845486/7668803/cc0a9178-fc87-11e4-9d0e-9fd32ea3c1fc.gif) 19 | 20 | ## Features 21 | 22 | - Convert from CSV/TSV. 23 | - Auto formatting. 24 | - Support multibyte contents. 25 | - Define align at CSV/TSV header ex. `:headerA:, headerB:` 26 | - Repeating execution. 27 | - Specify a header line (`-H` option). 28 | 29 | See also [examples](https://godoc.org/github.com/monochromegane/mdt#pkg-examples). 30 | 31 | ## Installation 32 | 33 | ```sh 34 | $ go get github.com/monochromegane/mdt/... 35 | ``` 36 | 37 | ## Integration 38 | 39 | ### Automator 40 | 41 | ![mdt\_with\_automator](https://cloud.githubusercontent.com/assets/1845486/7668851/5d833f84-fc8c-11e4-8787-aa39ce6ab300.png) 42 | 43 | 1. Create new Automator Service. 44 | 2. Select `Run Shell Script` action. 45 | 3. `Service receives selected` -> `text` 46 | 4. Check `Output replaces selected text` 47 | 5. Input `/path/to/mdt` at `Run Shell Script` (`pass input` is `to stdin`). 48 | 49 | You can call the service by shortcut key. 50 | 51 | `System Preferences` > `Keyboard` > `Shortcuts` tab > `Services` > Select your service, and set shortcut. 52 | 53 | ## Contribution 54 | 55 | 1. Fork it 56 | 2. Create a feature branch 57 | 3. Commit your changes 58 | 4. Rebase your local changes against the master branch 59 | 5. Run test suite with the `go test ./...` command and confirm that it passes 60 | 6. Run `gofmt -s` 61 | 7. Create new Pull Request 62 | 63 | ## License 64 | 65 | [MIT](https://github.com/monochromegane/mdt/blob/master/LICENSE) 66 | 67 | ## Author 68 | 69 | [monochromegane](https://github.com/monochromegane) 70 | 71 | -------------------------------------------------------------------------------- /cmd/mdt/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "os" 8 | 9 | "github.com/monochromegane/mdt" 10 | ) 11 | 12 | var header string 13 | 14 | func init() { 15 | flag.StringVar(&header, "H", "", "Specify a header line. If none specified, then first line is used as a header line.") 16 | flag.Parse() 17 | } 18 | 19 | func main() { 20 | out, err := mdt.Convert(header, os.Stdin) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | fmt.Printf("%s", out) 25 | } 26 | -------------------------------------------------------------------------------- /cmd/mdt/mdt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monochromegane/mdt/3ae3b51eea5d6fed07ee6554b9fbb8c282aa9a82/cmd/mdt/mdt -------------------------------------------------------------------------------- /column.go: -------------------------------------------------------------------------------- 1 | package mdt 2 | 3 | import ( 4 | "fmt" 5 | "github.com/mattn/go-runewidth" 6 | "strings" 7 | ) 8 | 9 | type columner interface { 10 | length() int 11 | toMarkdown(length int) string 12 | } 13 | 14 | type column struct { 15 | content string 16 | } 17 | 18 | func newColumn(s string) column { 19 | return column{ 20 | content: strings.TrimSpace(s), 21 | } 22 | } 23 | 24 | func (c column) length() int { 25 | return runewidth.StringWidth(c.content) 26 | } 27 | 28 | func (c column) toMarkdown(length int) string { 29 | return " " + c.content + strings.Repeat(" ", length-c.length()) + " " 30 | } 31 | 32 | type align int 33 | 34 | const ( 35 | left align = iota 36 | center 37 | right 38 | none 39 | ) 40 | 41 | type divColumn struct { 42 | column 43 | align align 44 | } 45 | 46 | func newDivColumn(s string) divColumn { 47 | s = strings.TrimSpace(s) 48 | align := none 49 | switch { 50 | case strings.HasPrefix(s, ":") && strings.HasSuffix(s, ":"): 51 | align = center 52 | case strings.HasPrefix(s, ":"): 53 | align = left 54 | case strings.HasSuffix(s, ":"): 55 | align = right 56 | } 57 | return divColumn{ 58 | column: newColumn(strings.Trim(s, ":")), 59 | align: align, 60 | } 61 | } 62 | 63 | func (d divColumn) toMarkdown(length int) string { 64 | prefix := " " 65 | suffix := " " 66 | switch d.align { 67 | case left: 68 | prefix = ":" 69 | case center: 70 | prefix = ":" 71 | suffix = ":" 72 | case right: 73 | suffix = ":" 74 | } 75 | return fmt.Sprintf("%s%s%s", prefix, strings.Repeat("-", length), suffix) 76 | } 77 | -------------------------------------------------------------------------------- /mdt.go: -------------------------------------------------------------------------------- 1 | package mdt 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | ) 7 | 8 | func Convert(header string, r io.Reader) (string, error) { 9 | 10 | table := newTable() 11 | 12 | if header != "" { 13 | table.addRow(header) 14 | } 15 | 16 | scanner := bufio.NewScanner(r) 17 | for scanner.Scan() { 18 | table.addRow(scanner.Text()) 19 | } 20 | 21 | if err := scanner.Err(); err != nil { 22 | return "", err 23 | } 24 | 25 | return table.toMarkdown(), nil 26 | } 27 | -------------------------------------------------------------------------------- /mdt_test.go: -------------------------------------------------------------------------------- 1 | package mdt_test 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/monochromegane/mdt" 8 | ) 9 | 10 | func ExampleConvert_csv() { 11 | r := strings.NewReader(` 12 | headerA, headerB 13 | content, content 14 | `) 15 | result, _ := mdt.Convert("", r) 16 | fmt.Printf("%s", result) 17 | 18 | // Output: 19 | // | headerA | headerB | 20 | // | ------- | ------- | 21 | // | content | content | 22 | } 23 | 24 | func ExampleConvert_tsv() { 25 | r := strings.NewReader(` 26 | headerA headerB 27 | content content 28 | `) 29 | result, _ := mdt.Convert("", r) 30 | fmt.Printf("%s", result) 31 | 32 | // Output: 33 | // | headerA | headerB | 34 | // | ------- | ------- | 35 | // | content | content | 36 | } 37 | 38 | func ExampleConvert_format() { 39 | r := strings.NewReader(` 40 | headerA, headerB 41 | short, very very long content 42 | `) 43 | result, _ := mdt.Convert("", r) 44 | fmt.Printf("%s", result) 45 | 46 | // Output: 47 | // | headerA | headerB | 48 | // | ------- | ---------------------- | 49 | // | short | very very long content | 50 | } 51 | 52 | func ExampleConvert_multibyte() { 53 | r := strings.NewReader(` 54 | headerA, headerB 55 | マルチバイト文字, content 56 | マルチバイト文字, content 57 | `) 58 | result, _ := mdt.Convert("", r) 59 | fmt.Printf("%s", result) 60 | 61 | // Output: 62 | // | headerA | headerB | 63 | // | ---------------- | ------- | 64 | // | マルチバイト文字 | content | 65 | // | マルチバイト文字 | content | 66 | } 67 | 68 | func ExampleConvert_align() { 69 | r := strings.NewReader(` 70 | headerA:, :headerB: 71 | content, content 72 | `) 73 | result, _ := mdt.Convert("", r) 74 | fmt.Printf("%s", result) 75 | 76 | // Output: 77 | // | headerA | headerB | 78 | // | -------:|:-------:| 79 | // | content | content | 80 | } 81 | 82 | func ExampleConvert_repeat() { 83 | r := strings.NewReader(` 84 | | headerA | headerB | 85 | | -------:|:-------:| 86 | | content | content | 87 | next content, next content 88 | `) 89 | result, _ := mdt.Convert("", r) 90 | fmt.Printf("%s", result) 91 | 92 | // Output: 93 | // | headerA | headerB | 94 | // | ------------:|:------------:| 95 | // | content | content | 96 | // | next content | next content | 97 | } 98 | 99 | func ExampleConvert_short() { 100 | r := strings.NewReader(` 101 | #,A 102 | 1,B 103 | `) 104 | result, _ := mdt.Convert("", r) 105 | fmt.Printf("%s", result) 106 | 107 | // Output: 108 | // | # | A | 109 | // | --- | --- | 110 | // | 1 | B | 111 | } 112 | 113 | func ExampleConvert_short_align() { 114 | r := strings.NewReader(` 115 | #:,:A: 116 | 1,B 117 | `) 118 | result, _ := mdt.Convert("", r) 119 | fmt.Printf("%s", result) 120 | 121 | // Output: 122 | // | # | A | 123 | // | ---:|:---:| 124 | // | 1 | B | 125 | } 126 | 127 | func ExampleConvert_with_header() { 128 | r := strings.NewReader(` 129 | content, content 130 | `) 131 | result, _ := mdt.Convert("headerA,headerB", r) 132 | fmt.Printf("%s", result) 133 | 134 | // Output: 135 | // | headerA | headerB | 136 | // | ------- | ------- | 137 | // | content | content | 138 | } 139 | 140 | func ExampleConvert_with_header_align() { 141 | r := strings.NewReader(` 142 | content, content 143 | `) 144 | result, _ := mdt.Convert("headerA:,:headerB", r) 145 | fmt.Printf("%s", result) 146 | 147 | // Output: 148 | // | headerA | headerB | 149 | // | -------:|:------- | 150 | // | content | content | 151 | } 152 | -------------------------------------------------------------------------------- /row.go: -------------------------------------------------------------------------------- 1 | package mdt 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type row struct { 9 | columns []columner 10 | newColumn newColumnFunc 11 | } 12 | 13 | type newColumnFunc func(s string) columner 14 | 15 | func newHeaderRow(s string) (row, row) { 16 | divRow := newDivRow(s) 17 | row := row{newColumn: func(s string) columner { return newColumn(s) }} 18 | 19 | for _, dc := range divRow.columns { 20 | c := dc.(divColumn) 21 | row.columns = append(row.columns, c.column) 22 | } 23 | return row, divRow 24 | } 25 | 26 | func newDivRow(s string) row { 27 | row := row{newColumn: func(s string) columner { return newDivColumn(s) }} 28 | row.setRow(s) 29 | return row 30 | } 31 | 32 | func newRow(s string) row { 33 | row := row{newColumn: func(s string) columner { return newColumn(s) }} 34 | row.setRow(s) 35 | return row 36 | } 37 | 38 | func (r *row) setRow(s string) { 39 | s = strings.Trim(s, "|") 40 | s = strings.Replace(s, "|", "\t", -1) 41 | s = strings.Replace(s, ",", "\t", -1) 42 | 43 | for _, col := range strings.Split(s, "\t") { 44 | r.columns = append(r.columns, r.newColumn(col)) 45 | } 46 | } 47 | 48 | func (r row) colNum() int { 49 | return len(r.columns) 50 | } 51 | 52 | func (r row) lengthAt(index int) int { 53 | if l := len(r.columns) - 1; index > l { 54 | return 0 55 | } else { 56 | return r.columns[index].length() 57 | } 58 | } 59 | 60 | func (r *row) fillColumn(colNum int) { 61 | short := colNum - len(r.columns) 62 | for i := 0; i < short; i++ { 63 | r.columns = append(r.columns, r.newColumn("")) 64 | } 65 | } 66 | 67 | func (r row) toMarkdown(colNum int, lens []int) string { 68 | r.fillColumn(colNum) 69 | columns := []string{} 70 | for i, c := range r.columns { 71 | columns = append(columns, c.toMarkdown(lens[i])) 72 | } 73 | 74 | return fmt.Sprintf("|%s|", strings.Join(columns, "|")) 75 | } 76 | -------------------------------------------------------------------------------- /rows.go: -------------------------------------------------------------------------------- 1 | package mdt 2 | 3 | import "bytes" 4 | 5 | type rows []row 6 | 7 | func (r rows) colNum() int { 8 | var max int 9 | for _, row := range r { 10 | if n := row.colNum(); n > max { 11 | max = n 12 | } 13 | } 14 | return max 15 | } 16 | 17 | func (r rows) lengthAt(index int) int { 18 | var max int 19 | for _, row := range r { 20 | if n := row.lengthAt(index); n > max { 21 | max = n 22 | } 23 | } 24 | if max < 3 { 25 | max = 3 26 | } 27 | return max 28 | } 29 | 30 | func (r rows) toMarkdown() string { 31 | colNum := r.colNum() 32 | lens := []int{} 33 | for i := 0; i < colNum; i++ { 34 | lens = append(lens, r.lengthAt(i)) 35 | } 36 | 37 | var buf bytes.Buffer 38 | for _, row := range r { 39 | buf.WriteString(row.toMarkdown(colNum, lens) + "\n") 40 | } 41 | return buf.String() 42 | } 43 | -------------------------------------------------------------------------------- /table.go: -------------------------------------------------------------------------------- 1 | package mdt 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | type table struct { 9 | rows rows 10 | } 11 | 12 | func newTable() table { 13 | return table{} 14 | } 15 | 16 | func (t *table) addRow(s string) { 17 | s = strings.TrimSpace(s) 18 | // skip blank row. 19 | if s == "" { 20 | return 21 | } 22 | 23 | if len(t.rows) == 0 { 24 | // header and division rows. 25 | row, divRow := newHeaderRow(s) 26 | t.rows = append(t.rows, row) 27 | t.rows = append(t.rows, divRow) 28 | } else { 29 | if isDivRow(s) { 30 | // over write division row. 31 | t.rows[1] = newDivRow(s) 32 | } else { 33 | // content row. 34 | t.rows = append(t.rows, newRow(s)) 35 | } 36 | } 37 | } 38 | 39 | func (t *table) toMarkdown() string { 40 | return t.rows.toMarkdown() 41 | } 42 | 43 | var regDivRow = regexp.MustCompile("^[\\s]*\\|[\\s]*:*--+[\\s]*:*\\|") 44 | 45 | func isDivRow(s string) bool { 46 | return regDivRow.MatchString(s) 47 | } 48 | --------------------------------------------------------------------------------