From d4d9394a1f699d29029eac8b1e8a74ec8c6e231d Mon Sep 17 00:00:00 2001 From: William Perron Date: Thu, 19 Oct 2023 16:02:56 -0400 Subject: [PATCH] add markdown-to-csv option --- cmd/md-fmt/main.go | 87 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 12 deletions(-) diff --git a/cmd/md-fmt/main.go b/cmd/md-fmt/main.go index 53383ff..8df8e82 100644 --- a/cmd/md-fmt/main.go +++ b/cmd/md-fmt/main.go @@ -3,11 +3,11 @@ package main import ( + "bufio" "encoding/csv" "flag" "fmt" "io" - "log" "os" "strings" ) @@ -16,6 +16,7 @@ var ( sourcePath = flag.String("source", "", "path to the input file") separator = flag.String("sep", ",", "separator character to use when reading the csv file") lazyQuotes = flag.Bool("lazy-quotes", false, "controls the lazy-quotes setting on the csv reader") + toCSV = flag.Bool("to-csv", false, "parses markdown table and encodes it to csv (opposite operation)") ) func main() { @@ -23,18 +24,27 @@ func main() { fd, err := os.Open(*sourcePath) if err != nil { - log.Fatalf("failed to open source file %s: %s\n", *sourcePath, err) + fmt.Fprintf(os.Stderr, "failed to open source file %s: %s\n", *sourcePath, err) } + if *toCSV { + markdownToCSV(fd, os.Stdout) + } else { + csvToMarkdown(fd, os.Stdout) + } +} + +func csvToMarkdown(r io.ReadSeeker, w io.Writer) { sep := []rune(*separator)[0] - read := csv.NewReader(fd) + read := csv.NewReader(r) read.Comma = sep read.TrimLeadingSpace = true read.LazyQuotes = *lazyQuotes rec, err := read.Read() if err != nil { - log.Fatalf("error reading from csv file: %s", err) + fmt.Fprintf(os.Stderr, "error reading from csv file: %s\n", err) + return } widths := make([]int, len(rec)) @@ -47,7 +57,8 @@ func main() { break } if err != nil { - log.Fatalf("error reading from csv file: %s", err) + fmt.Fprintf(os.Stderr, "error reading from csv file: %s", err) + return } for i, col := range rec { @@ -62,8 +73,8 @@ func main() { pattern := fmt.Sprintf("|%s|\n", strings.Join(c, "|")) // Reset file descriptor cursor and take new CSV reader from it - fd.Seek(0, 0) - read = csv.NewReader(fd) + r.Seek(0, 0) + read = csv.NewReader(r) read.Comma = sep read.TrimLeadingSpace = true read.LazyQuotes = *lazyQuotes @@ -71,21 +82,22 @@ func main() { // Format header row rec, err = read.Read() if err != nil { - log.Fatalf("failed to read next csv record: %s", err) + fmt.Fprintf(os.Stderr, "failed to read next csv record: %s", err) + return } curr := make([]any, 0, 2*len(widths)) for i := range widths { curr = append(curr, widths[i], rec[i]) } - fmt.Printf(pattern, curr...) + fmt.Fprintf(w, pattern, curr...) // Format header separator row curr = curr[:0] // empty slice but preserve capacity for i := range widths { curr = append(curr, widths[i], strings.Repeat("-", widths[i])) } - fmt.Printf(pattern, curr...) + fmt.Fprintf(w, pattern, curr...) for { rec, err := read.Read() @@ -93,14 +105,65 @@ func main() { break } if err != nil { - log.Fatalf("error reading from csv file: %s", err) + fmt.Fprintf(os.Stderr, "error reading from csv file: %s", err) + return } curr = curr[:0] for i := range widths { curr = append(curr, widths[i], rec[i]) } - fmt.Printf(pattern, curr...) + fmt.Fprintf(w, pattern, curr...) + } +} + +func markdownToCSV(r io.ReadSeeker, w io.Writer) { + cw := csv.NewWriter(w) + defer func() { + cw.Flush() + if err := cw.Error(); err != nil { + fmt.Fprintf(os.Stderr, "error occured during scan: %s", err) + } + }() + + // for now we're assuming there's only one table in a markdown file, this + // flag keeps track of whether or not we've seen a table already, and is + // used to exit the loop early once we're done reading the table, ignoring + // subsequent lines in the file. + var seen bool + scanner := bufio.NewScanner(r) + for scanner.Scan() { + if err := scanner.Err(); err != nil { + fmt.Fprintln(os.Stderr, "reading standard input:", err) + } + + line := strings.TrimSpace(scanner.Text()) + if !(strings.HasPrefix(line, "|") && strings.HasSuffix(line, "|")) { + if seen { + break // exit early if we've seen a table already + } + continue // keep going until we see a table + } + + // the rest of the body only applies to lines that are part of a table + // i.e. that start and end with a pipe character (`|`) + seen = true + + line = strings.Trim(line, "|") + parts := strings.Split(line, "|") + for i := 0; i < len(parts); i++ { + parts[i] = strings.TrimSpace(parts[i]) + } + + // ignore separator line + if parts[0] == strings.Repeat("-", len(parts[0])) { + continue + } + + if err := cw.Write(parts); err != nil { + fmt.Fprintf(os.Stderr, "failed to write to output: %s\n", err) + return + } } }