Skip to content

Commit

Permalink
V1
Browse files Browse the repository at this point in the history
  • Loading branch information
navnitms committed Sep 18, 2024
1 parent dfa0b35 commit 2124341
Show file tree
Hide file tree
Showing 10 changed files with 1,838 additions and 0 deletions.
124 changes: 124 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Description: This file contains the root command for the CLI application.
package cmd

import (
"bufio"
"fmt"
"log"
"os"
"strings"
"github.com/navnitms/csvtohtml/converter"
"github.com/spf13/cobra"
)

var title, delimiter, quotechar, height string
var displayLength int
var overwrite, serve, pagination, noHeader, export bool

var rootCmd = &cobra.Command{
Use: "csvtohtml",
Short: "CLI Application to convert CSV file to HTML table",
Long: `csvtohtml is a CLI tool designed to convert CSV files into HTML tables with ease.
It provides various customization options for generating the HTML table, including control
over the table's appearance and features such as pagination, virtual scrolling, and export functionality.
You can use this application by specifying the input CSV file and optionally the output HTML file.
Examples:
Convert a CSV file to an HTML table:
csvtohtml input.csv output.html
Convert and display the HTML table directly in a browser:
csvtohtml input.csv --serve
Flags for customization:
-t, --title: Set a title for the HTML table.
-d, --delimiter: Specify the delimiter used in the CSV file. Default is ','.
-q, --quotechar: Specify the character used for quoting fields with special characters. Default is '"'.
-dl, --display-length: Set the number of rows to display by default. Default is -1 (show all rows).
-o, --overwrite: Overwrite the output file if it already exists.
-s, --serve: Open the generated HTML in the browser instead of saving to a file.
-h, --height: Set the table height (in px or %) for the generated HTML.
-p, --pagination: Enable or disable pagination for the table. Default is enabled.
-nh, --no-header: Disable treating the first row as headers.
-e, --export: Enable export options for filtered rows. Default is enabled.`,

Run: func(cmd *cobra.Command, args []string) {
inputFile := args[0]
outputFile := ""
if len(args) > 1 {
outputFile = args[1]
}

options := map[string]interface{}{
"title": title,
"delimiter": delimiter,
"quotechar": quotechar,
"display_length": displayLength,
"height": height,
"pagination": pagination,
"no_header": noHeader,
"export": export,
}

// Convert CSV to HTML content
content, err := converter.Convert(inputFile, options)
if err != nil {
log.Fatalf("Failed to convert: %v", err)
}

if serve {
// Serve the HTML in the browser
converter.Serve(content)
} else if outputFile != "" {
// Check if file should be overwritten
if !overwrite && !promptOverwrite(outputFile) {
log.Fatal("Operation aborted.")
}
// Save the output
err := converter.Save(outputFile, content)
if err != nil {
log.Fatalf("Failed to save file: %v", err)
}
fmt.Printf("File converted successfully: %s\n", outputFile)
} else {
log.Fatal("Missing argument \"output_file\".")
}
},
}


func init() {
rootCmd.Flags().StringVarP(&title, "title", "t", "", "Table title")
rootCmd.Flags().StringVarP(&delimiter, "delimiter", "d", ",", "CSV delimiter")
rootCmd.Flags().StringVarP(&quotechar, "quotechar", "q", `"`, "String used to quote fields containing special characters")
rootCmd.Flags().IntVarP(&displayLength, "length", "l", -1, "Number of rows to show by default. Defaults to -1 (show all rows)")
rootCmd.Flags().BoolVarP(&overwrite, "overwrite", "o", false, "Overwrite the output file if exists")
rootCmd.Flags().BoolVarP(&serve, "serve", "s", false, "Open output HTML in browser instead of writing to file")
rootCmd.Flags().StringVarP(&height, "height", "H", "", "Table height in px or %") // Changed shorthand to "H"
rootCmd.Flags().BoolVarP(&pagination, "pagination", "p", true, "Enable/disable table pagination")
rootCmd.Flags().BoolVar(&noHeader, "no-header", false, "Disable displaying first row as headers")
rootCmd.Flags().BoolVarP(&export, "export", "e", true, "Enable filtered rows export options")
}


func promptOverwrite(fileName string) bool {
if _, err := os.Stat(fileName); os.IsNotExist(err) {
return true
}

reader := bufio.NewReader(os.Stdin)
fmt.Printf("File (%s) already exists. Do you want to overwrite? (y/n): ", fileName)
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)

if input != "y" && input != "Y" {
return false
}

return true
}

func Execute() error {
return rootCmd.Execute()
}
140 changes: 140 additions & 0 deletions converter/converter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package converter

import (
"os"
"fmt"
"time"
"encoding/csv"
"path/filepath"
"regexp"
"html/template"
"bytes"
"strings"
"github.com/pkg/browser"
)

var (
packagePath, _ = os.Getwd()
templatesDir = filepath.Join(packagePath, "templates")
jsSrcPattern = regexp.MustCompile(`<script.*?src=\"(.*?)\".*?<\/script>`)
jsFilesPath = filepath.Join(packagePath, "templates")
templateFile = "template.txt"
)

func Convert(inputFileName string, options map[string]interface{}) (string, error) {
delimiter := getStringOption(options, "delimiter", ",")
// quotechar := getStringOption(options, "quotechar", "\"")

file, err := os.Open(inputFileName)
if err != nil {
return "", fmt.Errorf("failed to open input file: %v", err)
}
defer file.Close()

reader := csv.NewReader(file)
reader.Comma = rune(delimiter[0])
reader.LazyQuotes = true

var csvHeaders []string
var csvRows [][]string

fmt.Println(options["title"])
if options["no_header"] == false {
csvHeaders, err = reader.Read()
if err != nil {
return "", fmt.Errorf("failed to read CSV header: %v", err)
}
}

csvRows, err = reader.ReadAll()
if err != nil {
return "", fmt.Errorf("failed to read CSV rows: %v", err)
}
html, err := renderTemplate(csvHeaders, csvRows, options)
if err != nil {
return "", fmt.Errorf("failed to render template: %v", err)
}

return replaceScript(html) , nil
}


func renderTemplate(headers []string, rows [][]string, options map[string]interface{}) (string, error) {
tmplPath := filepath.Join(templatesDir, templateFile)
tmpl, err := template.ParseFiles(tmplPath)
if err != nil {
return "", fmt.Errorf("failed to parse template: %v", err)
}


data := map[string]interface{}{
"Title": getStringOption(options, "title", "Table"),
"Headers": headers,
"Rows": rows, // Now rows is [][]string
"Pagination": options["pagination"],
"TableHeight": getStringOption(options, "height", "70vh"),
"DisplayLength": getIntOption(options, "display_length", -1),
}

fmt.Print(data["Title"])
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
return "", fmt.Errorf("failed to render template: %v", err)
}

return buf.String(), nil
}




func replaceScript(html string) string {
matches := jsSrcPattern.FindAllStringSubmatch(html, -1)

if len(matches) == 0 {
return html
}

for _, match := range matches {
src := match[1]

if strings.HasPrefix(src, "http://") || strings.HasPrefix(src, "https://") {
continue
}

filePath := filepath.Join(jsFilesPath, src)
fileContent, err := os.ReadFile(filePath)
if err != nil {
fmt.Println(fmt.Errorf("failed to read JS file: %v", err))
}

jsContent := fmt.Sprintf("<script type=\"text/javascript\">%s</script>", fileContent)
html = strings.Replace(html, match[0], jsContent, 1)
}

return html
}

// Save saves content to a file
func Save(fileName, content string) error {
return os.WriteFile(fileName, []byte(content), 0644)
}

// Serve serves the content in a temporary HTML file and opens it in a browser
func Serve(content string) {
tmpFile, err := os.CreateTemp("", "csvtotable_*.html")
if err != nil {
fmt.Printf("Failed to create temporary file: %v", err)
}
defer os.Remove(tmpFile.Name())

if _, err := tmpFile.Write([]byte(content)); err != nil {
fmt.Printf("Failed to write to temp file: %v", err)
}

browser.OpenFile(tmpFile.Name())

for {
time.Sleep(time.Second)
}
}
19 changes: 19 additions & 0 deletions converter/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package converter

func getStringOption(options map[string]interface{}, key, defaultValue string) string {
if val, ok := options[key]; ok {
if str, ok := val.(string); ok {
return str
}
}
return defaultValue
}

func getIntOption(options map[string]interface{}, key string, defaultValue int) int {
if val, ok := options[key]; ok {
if i, ok := val.(int); ok {
return i
}
}
return defaultValue
}
14 changes: 14 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module github.com/navnitms/csvtohtml

go 1.22.0

require (
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/spf13/cobra v1.8.1
)

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.1.0 // indirect
)
14 changes: 14 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
7 changes: 7 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package main

import "github.com/navnitms/csvtohtml/cmd"

func main() {
cmd.Execute()
}
Loading

0 comments on commit 2124341

Please sign in to comment.