1package fileio23import (4 "context"5 "io"6 "path/filepath"7 "strings"8)910// Kind is an external file format.11type Kind int1213const (14 CSV Kind = iota + 115 TSV16 XLSX17 SQLite18 Parquet19 WK120 JSON21 NUON22 HTML23)2425// A fileFormat is everything about a Kind: its names, its extensions,26// how the UI offers it, and the functions reading and writing it. Import,27// Export, KindOf and every list of formats the UI shows come from the28// kinds table, so adding a format is a file with its importer (and29// exporter) and a row there.30type fileFormat struct {31 kind Kind32 name string // short name, e.g. in "Download as CSV"33 noun string // what people call it in a sentence, e.g. "Excel"34 label string // what the file is, for the import picker35 exts []string // recognized extensions, the first one written36 read importer3738 // Exporting; write is nil for formats 012 only imports.39 write exporter40 menu string // the Download menu's title, without the extension41 about string // what downloading writes, for the command's description42 // encode is set for text formats, which Encode writes to a stream43 // (012 --pipe's standard output); it returns the rows written.44 encode func(io.Writer, *Snapshot) (int, error)4546 // book is set for formats that hold named sheets of their own:47 // every sheet is exported, and an imported file keeps its sheets'48 // names. A file of any other kind has one sheet, named after it.49 book bool50 // tables is set for databases: an import picks a table, and an51 // export writes the sheet or the selection as a named table.52 tables bool53 // widths is set for formats that keep column widths: an import54 // keeps them, where the other formats' columns are fitted to their55 // data (Sheet.LoadFitWidths).56 widths bool57}5859// An importer reads a file into a new workbook, returning the sheet to60// show. It checks ctx between rows and updates opt.Progress.61type importer func(ctx context.Context, name string, opt Options) (*Result, error)6263// An exporter writes a snapshot to a file.64type exporter func(ctx context.Context, name string, snap *Snapshot, opt ExportOptions) (*ExportResult, error)6566// kinds are the formats, in Kind order, which is menu order.67var kinds = []fileFormat{{68 kind: CSV, name: "CSV", noun: "CSV", label: "Comma-separated values", exts: []string{".csv"},69 read: importCSV, write: exportCSV, menu: "Comma-separated values", encode: encodeCSV,70 about: "Save the values as shown, as comma-separated values (.csv)",71}, {72 kind: TSV, name: "TSV", noun: "TSV", label: "Tab-separated values", exts: []string{".tsv", ".tab"},73 read: importTSV, write: exportTSV, menu: "Tab-separated values", encode: encodeTSV,74 about: "Save the values as shown, as tab-separated values (.tsv)",75}, {76 kind: XLSX, name: "XLSX", noun: "Excel", label: "Excel workbook", exts: []string{".xlsx", ".xlsm"},77 read: importXLSX, write: exportXLSX, menu: "Microsoft Excel",78 about: "Save as an Excel workbook (.xlsx) with formulas, formats and widths",79 book: true, widths: true,80}, {81 kind: SQLite, name: "SQLite", noun: "SQLite", label: "SQLite database", exts: []string{".sqlite", ".sqlite3", ".db"},82 read: importSQLite, write: exportSQLite, menu: "SQLite database",83 about: "Save the sheet, or the selection, as a table in a SQLite database",84 tables: true,85}, {86 kind: Parquet, name: "Parquet", noun: "Parquet", label: "Parquet file", exts: []string{".parquet"},87 read: importParquet,88}, {89 kind: WK1, name: "WK1", noun: "1-2-3", label: "Lotus 1-2-3 worksheet", exts: []string{".wk1", ".wks"},90 read: importWK1, widths: true,91}, {92 kind: JSON, name: "JSON", noun: "JSON", label: "JSON list of records", exts: []string{".json", ".ndjson", ".jsonl"},93 read: importJSON, write: exportJSON, menu: "JSON list of records", encode: encodeJSON,94 about: "Save the sheet as a JSON list of records, named by its first row",95}, {96 kind: NUON, name: "NUON", noun: "NUON", label: "Nushell table (NUON)", exts: []string{".nuon"},97 read: importNUON, write: exportNUON, menu: "Nushell table", encode: encodeNUON,98 about: "Save the sheet as a nushell table that keeps its types, named by its first row",99}, {100 kind: HTML, name: "HTML", noun: "HTML", label: "Web page", exts: []string{".html", ".htm"},101 write: exportHTML, menu: "Web page",102 about: "Save the sheet as a web page in 012's look, with its charts, to share with anyone",103}}104105// unknownFormat is what an invalid Kind reports.106var unknownFormat = fileFormat{name: "unknown", exts: []string{""}}107108// format returns the kind's row of the table.109func (k Kind) format() *fileFormat {110 if k < 1 || int(k) > len(kinds) {111 return &unknownFormat112 }113 return &kinds[k-1]114}115116// Kinds lists every format, in menu order.117func Kinds() []Kind {118 out := make([]Kind, len(kinds))119 for i, k := range kinds {120 out[i] = k.kind121 }122 return out123}124125// KindOf recognizes a file 012 imports by its extension.126func KindOf(name string) (Kind, bool) {127 k, ok := ExportKindOf(name)128 if !ok || !k.CanImport() {129 return 0, false130 }131 return k, true132}133134// ExportKindOf recognizes a file's format by its extension, including135// formats 012 only writes (HTML).136func ExportKindOf(name string) (Kind, bool) {137 ext := strings.ToLower(filepath.Ext(name))138 for _, k := range kinds {139 for _, e := range k.exts {140 if e == ext {141 return k.kind, true142 }143 }144 }145 return 0, false146}147148// CanImport reports whether 012 reads files of this kind.149func (k Kind) CanImport() bool { return k.format().read != nil }150151// String is the format's short name, e.g. "XLSX".152func (k Kind) String() string { return k.format().name }153154// Noun is what the format is called in a sentence, e.g. "Excel" in155// "Import a CSV, TSV, Excel ... file".156func (k Kind) Noun() string { return k.format().noun }157158// Label says what a file of this kind is, e.g. "Excel workbook".159func (k Kind) Label() string { return k.format().label }160161// Ext is the extension written for this kind, e.g. ".xlsx".162func (k Kind) Ext() string { return k.format().exts[0] }163164// CanExport reports whether sheets can be written in this format.165func (k Kind) CanExport() bool { return k.format().write != nil }166167// MenuTitle is the format's entry in the Download menu, e.g. "Microsoft168// Excel (.xlsx)"; empty for formats that can't be exported.169func (k Kind) MenuTitle() string {170 if !k.CanExport() {171 return ""172 }173 return k.format().menu + " (" + k.Ext() + ")"174}175176// About says what downloading in this format writes; empty for formats177// that can't be exported.178func (k Kind) About() string { return k.format().about }179180// HoldsSheets reports whether a file of this kind holds several named181// sheets, so a download writes the whole workbook (see SnapBook).182func (k Kind) HoldsSheets() bool { return k.format().book }183184// IsText reports whether this kind is text Encode writes to a stream:185// CSV, TSV, JSON or NUON.186func (k Kind) IsText() bool { return k.format().encode != nil }187188// KindNamed finds a kind by its short name, ignoring case: "nuon".189func KindNamed(name string) (Kind, bool) {190 for _, k := range kinds {191 if strings.EqualFold(k.name, name) {192 return k.kind, true193 }194 }195 return 0, false196}197198// HasTables reports whether a file of this kind is a database of tables:199// importing picks one (see Tables), and a download writes the sheet or200// the selection as a named table (ExportOptions.Table).201func (k Kind) HasTables() bool { return k.format().tables }