04a1f33cb1
- Full-screen Bubble Tea TUI with cream background fill using PadLine/FillBlankLines - Self-update command (--update) pulling from GitHub releases - install.sh for curl one-liner installation - Terminal Lovers section on web landing page with install command and CLI features - All 7 format categories, glob/directory batch support, auto-download ffmpeg
453 lines
12 KiB
Go
453 lines
12 KiB
Go
package converter
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
func convertData(inputPath, outputPath, sourceExt, targetFormat string) error {
|
|
// Strategy: parse input into a generic Go structure, then serialize to target format.
|
|
// For tabular data (CSV, TSV) we use [][]string -> []map[string]interface{} (first row = headers).
|
|
// For structured data (JSON, YAML, TOML, XML) we use interface{}.
|
|
|
|
raw, err := os.ReadFile(inputPath)
|
|
if err != nil {
|
|
return fmt.Errorf("reading input: %w", err)
|
|
}
|
|
|
|
// Determine if source is tabular or structured
|
|
switch sourceExt {
|
|
case "csv", "tsv":
|
|
return convertTabularData(raw, outputPath, sourceExt, targetFormat)
|
|
case "json":
|
|
var data interface{}
|
|
if err := json.Unmarshal(raw, &data); err != nil {
|
|
return fmt.Errorf("parsing JSON: %w", err)
|
|
}
|
|
return writeData(data, outputPath, targetFormat)
|
|
case "ndjson", "jsonl":
|
|
return convertNDJSON(raw, outputPath, targetFormat)
|
|
case "yaml", "yml":
|
|
var data interface{}
|
|
if err := yaml.Unmarshal(raw, &data); err != nil {
|
|
return fmt.Errorf("parsing YAML: %w", err)
|
|
}
|
|
return writeData(data, outputPath, targetFormat)
|
|
case "toml":
|
|
var data interface{}
|
|
if err := toml.Unmarshal(raw, &data); err != nil {
|
|
return fmt.Errorf("parsing TOML: %w", err)
|
|
}
|
|
return writeData(data, outputPath, targetFormat)
|
|
case "xml":
|
|
return convertXML(raw, outputPath, targetFormat)
|
|
case "ini", "env", "properties":
|
|
data := parseKeyValue(string(raw), sourceExt)
|
|
return writeData(data, outputPath, targetFormat)
|
|
case "sql":
|
|
return convertSQL(raw, outputPath, targetFormat)
|
|
default:
|
|
return fmt.Errorf("unsupported data source format: %s", sourceExt)
|
|
}
|
|
}
|
|
|
|
func convertTabularData(raw []byte, outputPath, sourceExt, targetFormat string) error {
|
|
delimiter := ','
|
|
if sourceExt == "tsv" {
|
|
delimiter = '\t'
|
|
}
|
|
|
|
reader := csv.NewReader(strings.NewReader(string(raw)))
|
|
reader.Comma = delimiter
|
|
reader.LazyQuotes = true
|
|
|
|
records, err := reader.ReadAll()
|
|
if err != nil {
|
|
return fmt.Errorf("parsing %s: %w", sourceExt, err)
|
|
}
|
|
|
|
if len(records) == 0 {
|
|
return fmt.Errorf("empty %s file", sourceExt)
|
|
}
|
|
|
|
// Convert to []map[string]interface{} using first row as headers
|
|
headers := records[0]
|
|
var rows []map[string]interface{}
|
|
for _, record := range records[1:] {
|
|
row := make(map[string]interface{})
|
|
for i, header := range headers {
|
|
if i < len(record) {
|
|
row[header] = record[i]
|
|
}
|
|
}
|
|
rows = append(rows, row)
|
|
}
|
|
|
|
switch targetFormat {
|
|
case "json":
|
|
return writeJSON(rows, outputPath)
|
|
case "yaml", "yml":
|
|
return writeYAML(rows, outputPath)
|
|
case "toml":
|
|
wrapper := map[string]interface{}{"data": rows}
|
|
return writeTOML(wrapper, outputPath)
|
|
case "xml":
|
|
return writeXMLFromRows(rows, outputPath)
|
|
case "tsv":
|
|
return writeDelimited(headers, records[1:], outputPath, '\t')
|
|
case "csv":
|
|
return writeDelimited(headers, records[1:], outputPath, ',')
|
|
case "html":
|
|
return writeHTMLTable(headers, records[1:], outputPath)
|
|
case "sql":
|
|
return writeSQLInserts(headers, records[1:], outputPath, "data")
|
|
case "ndjson":
|
|
return writeNDJSON(rows, outputPath)
|
|
default:
|
|
return fmt.Errorf("unsupported target format for tabular data: %s", targetFormat)
|
|
}
|
|
}
|
|
|
|
func convertNDJSON(raw []byte, outputPath, targetFormat string) error {
|
|
lines := strings.Split(strings.TrimSpace(string(raw)), "\n")
|
|
var items []interface{}
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
var item interface{}
|
|
if err := json.Unmarshal([]byte(line), &item); err != nil {
|
|
continue // skip invalid lines
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return writeData(items, outputPath, targetFormat)
|
|
}
|
|
|
|
func convertXML(raw []byte, outputPath, targetFormat string) error {
|
|
// Simple XML -> generic map conversion
|
|
var data interface{}
|
|
if err := xml.Unmarshal(raw, &data); err != nil {
|
|
// XML to map is tricky — treat as string content for simple cases
|
|
// or use a simple parser
|
|
data = map[string]interface{}{"xml_content": string(raw)}
|
|
}
|
|
return writeData(data, outputPath, targetFormat)
|
|
}
|
|
|
|
func convertSQL(raw []byte, outputPath, targetFormat string) error {
|
|
// Very basic: extract INSERT statement values
|
|
content := string(raw)
|
|
lines := strings.Split(content, "\n")
|
|
var records []map[string]interface{}
|
|
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
upper := strings.ToUpper(line)
|
|
if strings.HasPrefix(upper, "INSERT") {
|
|
// Very basic INSERT parser — extract values
|
|
valIdx := strings.Index(upper, "VALUES")
|
|
if valIdx == -1 {
|
|
continue
|
|
}
|
|
valPart := line[valIdx+6:]
|
|
valPart = strings.Trim(valPart, " ;()")
|
|
values := strings.Split(valPart, ",")
|
|
row := make(map[string]interface{})
|
|
for i, v := range values {
|
|
v = strings.TrimSpace(v)
|
|
v = strings.Trim(v, "'\"")
|
|
row[fmt.Sprintf("col%d", i+1)] = v
|
|
}
|
|
records = append(records, row)
|
|
}
|
|
}
|
|
|
|
if targetFormat == "json" {
|
|
return writeJSON(records, outputPath)
|
|
}
|
|
if targetFormat == "csv" {
|
|
// Flatten to CSV
|
|
if len(records) == 0 {
|
|
return os.WriteFile(outputPath, []byte(""), 0o644)
|
|
}
|
|
var headers []string
|
|
for k := range records[0] {
|
|
headers = append(headers, k)
|
|
}
|
|
var csvRecords [][]string
|
|
for _, r := range records {
|
|
var row []string
|
|
for _, h := range headers {
|
|
row = append(row, fmt.Sprintf("%v", r[h]))
|
|
}
|
|
csvRecords = append(csvRecords, row)
|
|
}
|
|
return writeDelimited(headers, csvRecords, outputPath, ',')
|
|
}
|
|
return fmt.Errorf("unsupported target format for SQL: %s", targetFormat)
|
|
}
|
|
|
|
func parseKeyValue(content, format string) map[string]interface{} {
|
|
result := make(map[string]interface{})
|
|
lines := strings.Split(content, "\n")
|
|
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, ";") {
|
|
continue
|
|
}
|
|
// Handle different separators
|
|
sep := "="
|
|
if format == "properties" && strings.Contains(line, ":") && !strings.Contains(line, "=") {
|
|
sep = ":"
|
|
}
|
|
parts := strings.SplitN(line, sep, 2)
|
|
if len(parts) == 2 {
|
|
key := strings.TrimSpace(parts[0])
|
|
value := strings.TrimSpace(parts[1])
|
|
// Remove quotes from .env values
|
|
if format == "env" {
|
|
value = strings.Trim(value, "\"'")
|
|
}
|
|
result[key] = value
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ─── Writers ─────────────────────────────────────────────────
|
|
|
|
func writeData(data interface{}, outputPath, targetFormat string) error {
|
|
switch targetFormat {
|
|
case "json":
|
|
return writeJSON(data, outputPath)
|
|
case "yaml", "yml":
|
|
return writeYAML(data, outputPath)
|
|
case "toml":
|
|
return writeTOML(data, outputPath)
|
|
case "csv":
|
|
return writeDataAsCSV(data, outputPath)
|
|
case "tsv":
|
|
return writeDataAsTSV(data, outputPath)
|
|
case "xml":
|
|
return writeXMLGeneric(data, outputPath)
|
|
case "html":
|
|
return writeDataAsHTML(data, outputPath)
|
|
case "ndjson":
|
|
return writeDataAsNDJSON(data, outputPath)
|
|
default:
|
|
return fmt.Errorf("unsupported target format: %s", targetFormat)
|
|
}
|
|
}
|
|
|
|
func writeJSON(data interface{}, outputPath string) error {
|
|
b, err := json.MarshalIndent(data, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(outputPath, b, 0o644)
|
|
}
|
|
|
|
func writeYAML(data interface{}, outputPath string) error {
|
|
b, err := yaml.Marshal(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return os.WriteFile(outputPath, b, 0o644)
|
|
}
|
|
|
|
func writeTOML(data interface{}, outputPath string) error {
|
|
f, err := os.Create(outputPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
enc := toml.NewEncoder(f)
|
|
return enc.Encode(data)
|
|
}
|
|
|
|
func writeDelimited(headers []string, rows [][]string, outputPath string, sep rune) error {
|
|
f, err := os.Create(outputPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
w := csv.NewWriter(f)
|
|
w.Comma = sep
|
|
if err := w.Write(headers); err != nil {
|
|
return err
|
|
}
|
|
for _, row := range rows {
|
|
if err := w.Write(row); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
w.Flush()
|
|
return w.Error()
|
|
}
|
|
|
|
func writeHTMLTable(headers []string, rows [][]string, outputPath string) error {
|
|
var sb strings.Builder
|
|
sb.WriteString("<table>\n<thead>\n<tr>")
|
|
for _, h := range headers {
|
|
sb.WriteString("<th>" + h + "</th>")
|
|
}
|
|
sb.WriteString("</tr>\n</thead>\n<tbody>\n")
|
|
for _, row := range rows {
|
|
sb.WriteString("<tr>")
|
|
for _, cell := range row {
|
|
sb.WriteString("<td>" + cell + "</td>")
|
|
}
|
|
sb.WriteString("</tr>\n")
|
|
}
|
|
sb.WriteString("</tbody>\n</table>")
|
|
return os.WriteFile(outputPath, []byte(sb.String()), 0o644)
|
|
}
|
|
|
|
func writeSQLInserts(headers []string, rows [][]string, outputPath, tableName string) error {
|
|
var sb strings.Builder
|
|
cols := strings.Join(headers, ", ")
|
|
for _, row := range rows {
|
|
var vals []string
|
|
for _, v := range row {
|
|
vals = append(vals, "'"+strings.ReplaceAll(v, "'", "''")+"'")
|
|
}
|
|
sb.WriteString(fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s);\n",
|
|
tableName, cols, strings.Join(vals, ", ")))
|
|
}
|
|
return os.WriteFile(outputPath, []byte(sb.String()), 0o644)
|
|
}
|
|
|
|
func writeNDJSON(rows []map[string]interface{}, outputPath string) error {
|
|
var sb strings.Builder
|
|
for _, row := range rows {
|
|
b, err := json.Marshal(row)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sb.Write(b)
|
|
sb.WriteByte('\n')
|
|
}
|
|
return os.WriteFile(outputPath, []byte(sb.String()), 0o644)
|
|
}
|
|
|
|
func writeXMLFromRows(rows []map[string]interface{}, outputPath string) error {
|
|
var sb strings.Builder
|
|
sb.WriteString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<data>\n")
|
|
for _, row := range rows {
|
|
sb.WriteString(" <row>\n")
|
|
for k, v := range row {
|
|
sb.WriteString(fmt.Sprintf(" <%s>%v</%s>\n", k, v, k))
|
|
}
|
|
sb.WriteString(" </row>\n")
|
|
}
|
|
sb.WriteString("</data>")
|
|
return os.WriteFile(outputPath, []byte(sb.String()), 0o644)
|
|
}
|
|
|
|
func writeXMLGeneric(data interface{}, outputPath string) error {
|
|
b, err := json.MarshalIndent(data, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Wrap JSON in XML as a simple approach
|
|
content := "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<data>\n" + string(b) + "\n</data>"
|
|
return os.WriteFile(outputPath, []byte(content), 0o644)
|
|
}
|
|
|
|
func writeDataAsCSV(data interface{}, outputPath string) error {
|
|
rows := toRowsOfMaps(data)
|
|
if len(rows) == 0 {
|
|
return os.WriteFile(outputPath, []byte(""), 0o644)
|
|
}
|
|
headers := extractHeaders(rows[0])
|
|
var records [][]string
|
|
for _, row := range rows {
|
|
var record []string
|
|
for _, h := range headers {
|
|
record = append(record, fmt.Sprintf("%v", row[h]))
|
|
}
|
|
records = append(records, record)
|
|
}
|
|
return writeDelimited(headers, records, outputPath, ',')
|
|
}
|
|
|
|
func writeDataAsTSV(data interface{}, outputPath string) error {
|
|
rows := toRowsOfMaps(data)
|
|
if len(rows) == 0 {
|
|
return os.WriteFile(outputPath, []byte(""), 0o644)
|
|
}
|
|
headers := extractHeaders(rows[0])
|
|
var records [][]string
|
|
for _, row := range rows {
|
|
var record []string
|
|
for _, h := range headers {
|
|
record = append(record, fmt.Sprintf("%v", row[h]))
|
|
}
|
|
records = append(records, record)
|
|
}
|
|
return writeDelimited(headers, records, outputPath, '\t')
|
|
}
|
|
|
|
func writeDataAsHTML(data interface{}, outputPath string) error {
|
|
b, err := json.MarshalIndent(data, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
html := "<html><body><pre>" + string(b) + "</pre></body></html>"
|
|
return os.WriteFile(outputPath, []byte(html), 0o644)
|
|
}
|
|
|
|
func writeDataAsNDJSON(data interface{}, outputPath string) error {
|
|
rows := toRowsOfMaps(data)
|
|
var sb strings.Builder
|
|
for _, row := range rows {
|
|
b, err := json.Marshal(row)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sb.Write(b)
|
|
sb.WriteByte('\n')
|
|
}
|
|
return os.WriteFile(outputPath, []byte(sb.String()), 0o644)
|
|
}
|
|
|
|
// ─── Helpers ─────────────────────────────────────────────────
|
|
|
|
func toRowsOfMaps(data interface{}) []map[string]interface{} {
|
|
switch v := data.(type) {
|
|
case []interface{}:
|
|
var rows []map[string]interface{}
|
|
for _, item := range v {
|
|
if m, ok := item.(map[string]interface{}); ok {
|
|
rows = append(rows, m)
|
|
}
|
|
}
|
|
return rows
|
|
case []map[string]interface{}:
|
|
return v
|
|
case map[string]interface{}:
|
|
return []map[string]interface{}{v}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func extractHeaders(row map[string]interface{}) []string {
|
|
var headers []string
|
|
for k := range row {
|
|
headers = append(headers, k)
|
|
}
|
|
return headers
|
|
}
|