Each row now also holds what the UI shows for the format (its noun, Download menu title and description) and its traits (holds several sheets, is a database of tables), exposed as Kind methods, so the UI can derive its lists instead of switching on kinds.
- commit
- 3945e7f2ef42ca3e7f094e63bf28101c28c14ee1
- parent
- 0040bb1c716ae3f7daff9e90e3e670f86f098fd8
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 111 | +--------------------- |
| A | internal/ | 151 | ++++++++++++++++++++++++++++++ |
| A | internal/ | 37 | +++++++ |
@@ -3,6 +3,7 @@
33 // 1-2-3 worksheets. Everything is pure Go. The sheet package knows none44 // of these formats: importers build a sheet through its public API55 // (Load, SetColWidth, RecalcAll) and exporters read a Snapshot of one.6+// Each format is a row of the table in formats.go and a file of its own.67 package fileio78 89 import (@@ -21,111 +22,6 @@ import (
2122 "github.com/FelineStateMachine/012/internal/telemetry"2223 )2324 24-// Kind is an external file format.25-type Kind int26-27-const (28- CSV Kind = iota + 129- TSV30- XLSX31- SQLite32- Parquet33- WK134-)35-36-// A fileFormat is everything about a Kind: its names, its extensions, and37-// the functions reading and writing it. Adding a format is a file with38-// its importer (and exporter) and a row here.39-type fileFormat struct {40- kind Kind41- name string // short name, e.g. in "Export as CSV"42- label string // what the file is, for the import picker43- exts []string // recognized extensions, the first one written44- // book is set for formats that hold named sheets of their own; a45- // file of any other kind names its one sheet after the file.46- book bool47- read importer48- // write is nil for formats 012 only imports.49- write exporter50-}51-52-// An importer reads a file into a new workbook, returning the sheet to53-// show. It checks ctx between rows, and updates opt.Progress.54-type importer func(ctx context.Context, name string, opt Options) (*Result, error)55-56-// An exporter writes a snapshot to a file.57-type exporter func(ctx context.Context, name string, snap *Snapshot, opt ExportOptions) (*ExportResult, error)58-59-// kinds are the formats, in Kind order, which is menu order.60-var kinds = []fileFormat{61- {CSV, "CSV", "Comma-separated values", []string{".csv"}, false, importCSV, exportCSV},62- {TSV, "TSV", "Tab-separated values", []string{".tsv", ".tab"}, false, importTSV, exportTSV},63- {XLSX, "XLSX", "Excel workbook", []string{".xlsx", ".xlsm"}, true, importXLSX, exportXLSX},64- {SQLite, "SQLite", "SQLite database", []string{".sqlite", ".sqlite3", ".db"}, false, importSQLite, exportSQLite},65- {Parquet, "Parquet", "Parquet file", []string{".parquet"}, false, importParquet, nil},66- {WK1, "WK1", "Lotus 1-2-3 worksheet", []string{".wk1", ".wks"}, false, importWK1, nil},67-}68-69-// format returns the kind's format, or nil for an unknown kind.70-func (k Kind) format() *fileFormat {71- if k < 1 || int(k) > len(kinds) {72- return nil73- }74- return &kinds[k-1]75-}76-77-// Kinds lists every format, in menu order.78-func Kinds() []Kind {79- out := make([]Kind, len(kinds))80- for i, k := range kinds {81- out[i] = k.kind82- }83- return out84-}85-86-// String is the format's short name, e.g. "XLSX".87-func (k Kind) String() string {88- if f := k.format(); f != nil {89- return f.name90- }91- return "unknown"92-}93-94-// Label says what a file of this kind is, e.g. "Excel workbook".95-func (k Kind) Label() string {96- if f := k.format(); f != nil {97- return f.label98- }99- return ""100-}101-102-// Ext is the extension written for this kind, e.g. ".xlsx".103-func (k Kind) Ext() string {104- if f := k.format(); f != nil {105- return f.exts[0]106- }107- return ""108-}109-110-// CanExport reports whether sheets can be written in this format.111-func (k Kind) CanExport() bool {112- f := k.format()113- return f != nil && f.write != nil114-}115-116-// KindOf recognizes a file's format by its extension.117-func KindOf(name string) (Kind, bool) {118- ext := strings.ToLower(filepath.Ext(name))119- for _, k := range kinds {120- for _, e := range k.exts {121- if e == ext {122- return k.kind, true123- }124- }125- }126- return 0, false127-}128-12925 // Progress reports how far an import has got. The importer updates it13026 // from its goroutine; the UI reads it on a timer.13127 type Progress struct {@@ -222,13 +118,12 @@ func Import(ctx context.Context, name string, opt Options) (*Result, error) {
222118 }223119 224120 func importKind(ctx context.Context, name string, k Kind, opt Options) (*Result, error) {225- f := k.format()226- r, err := f.read(ctx, name, opt)121+ r, err := k.format().read(ctx, name, opt)227122 if err != nil {228123 return nil, err229124 }230125 // A file of one table becomes one sheet named after it, as in Sheets.231- if book := r.Sheet.Book(); !f.book && book.Len() == 1 {126+ if book := r.Sheet.Book(); !k.HoldsSheets() && book.Len() == 1 {232127 base := strings.TrimSuffix(filepath.Base(name), filepath.Ext(name))233128 if opt.Table != "" {234129 base = opt.Table@@ -0,0 +1,151 @@
1+package fileio2+3+import (4+ "context"5+ "path/filepath"6+ "strings"7+)8+9+// Kind is an external file format.10+type Kind int11+12+const (13+ CSV Kind = iota + 114+ TSV15+ XLSX16+ SQLite17+ Parquet18+ WK119+)20+21+// A fileFormat is everything about a Kind: its names, its extensions,22+// how the UI offers it, and the functions reading and writing it. Import,23+// Export, KindOf and every list of formats the UI shows come from the24+// kinds table, so adding a format is a file with its importer (and25+// exporter) and a row there.26+type fileFormat struct {27+ kind Kind28+ name string // short name, e.g. in "Download as CSV"29+ noun string // what people call it in a sentence, e.g. "Excel"30+ label string // what the file is, for the import picker31+ exts []string // recognized extensions, the first one written32+ read importer33+34+ // Exporting; write is nil for formats 012 only imports.35+ write exporter36+ menu string // the Download menu's title, without the extension37+ about string // what downloading writes, for the command's description38+39+ // book is set for formats that hold named sheets of their own:40+ // every sheet is exported, and an imported file keeps its sheets'41+ // names. A file of any other kind has one sheet, named after it.42+ book bool43+ // tables is set for databases: an import picks a table, and an44+ // export writes the sheet or the selection as a named table.45+ tables bool46+}47+48+// An importer reads a file into a new workbook, returning the sheet to49+// show. It checks ctx between rows and updates opt.Progress.50+type importer func(ctx context.Context, name string, opt Options) (*Result, error)51+52+// An exporter writes a snapshot to a file.53+type exporter func(ctx context.Context, name string, snap *Snapshot, opt ExportOptions) (*ExportResult, error)54+55+// kinds are the formats, in Kind order, which is menu order.56+var kinds = []fileFormat{{57+ kind: CSV, name: "CSV", noun: "CSV", label: "Comma-separated values", exts: []string{".csv"},58+ read: importCSV, write: exportCSV, menu: "Comma-separated values",59+ about: "Save the values as shown, as comma-separated values (.csv)",60+}, {61+ kind: TSV, name: "TSV", noun: "TSV", label: "Tab-separated values", exts: []string{".tsv", ".tab"},62+ read: importTSV, write: exportTSV, menu: "Tab-separated values",63+ about: "Save the values as shown, as tab-separated values (.tsv)",64+}, {65+ kind: XLSX, name: "XLSX", noun: "Excel", label: "Excel workbook", exts: []string{".xlsx", ".xlsm"},66+ read: importXLSX, write: exportXLSX, menu: "Microsoft Excel",67+ about: "Save as an Excel workbook (.xlsx) with formulas, formats and widths",68+ book: true,69+}, {70+ kind: SQLite, name: "SQLite", noun: "SQLite", label: "SQLite database", exts: []string{".sqlite", ".sqlite3", ".db"},71+ read: importSQLite, write: exportSQLite, menu: "SQLite database",72+ about: "Save the sheet, or the selection, as a table in a SQLite database",73+ tables: true,74+}, {75+ kind: Parquet, name: "Parquet", noun: "Parquet", label: "Parquet file", exts: []string{".parquet"},76+ read: importParquet,77+}, {78+ kind: WK1, name: "WK1", noun: "1-2-3", label: "Lotus 1-2-3 worksheet", exts: []string{".wk1", ".wks"},79+ read: importWK1,80+}}81+82+// unknownFormat is what an invalid Kind reports.83+var unknownFormat = fileFormat{name: "unknown", exts: []string{""}}84+85+// format returns the kind's row of the table.86+func (k Kind) format() *fileFormat {87+ if k < 1 || int(k) > len(kinds) {88+ return &unknownFormat89+ }90+ return &kinds[k-1]91+}92+93+// Kinds lists every format, in menu order.94+func Kinds() []Kind {95+ out := make([]Kind, len(kinds))96+ for i, k := range kinds {97+ out[i] = k.kind98+ }99+ return out100+}101+102+// KindOf recognizes a file's format by its extension.103+func KindOf(name string) (Kind, bool) {104+ ext := strings.ToLower(filepath.Ext(name))105+ for _, k := range kinds {106+ for _, e := range k.exts {107+ if e == ext {108+ return k.kind, true109+ }110+ }111+ }112+ return 0, false113+}114+115+// String is the format's short name, e.g. "XLSX".116+func (k Kind) String() string { return k.format().name }117+118+// Noun is what the format is called in a sentence, e.g. "Excel" in119+// "Import a CSV, TSV, Excel ... file".120+func (k Kind) Noun() string { return k.format().noun }121+122+// Label says what a file of this kind is, e.g. "Excel workbook".123+func (k Kind) Label() string { return k.format().label }124+125+// Ext is the extension written for this kind, e.g. ".xlsx".126+func (k Kind) Ext() string { return k.format().exts[0] }127+128+// CanExport reports whether sheets can be written in this format.129+func (k Kind) CanExport() bool { return k.format().write != nil }130+131+// MenuTitle is the format's entry in the Download menu, e.g. "Microsoft132+// Excel (.xlsx)"; empty for formats that can't be exported.133+func (k Kind) MenuTitle() string {134+ if !k.CanExport() {135+ return ""136+ }137+ return k.format().menu + " (" + k.Ext() + ")"138+}139+140+// About says what downloading in this format writes; empty for formats141+// that can't be exported.142+func (k Kind) About() string { return k.format().about }143+144+// HoldsSheets reports whether a file of this kind holds several named145+// sheets, so a download writes the whole workbook (see SnapBook).146+func (k Kind) HoldsSheets() bool { return k.format().book }147+148+// HasTables reports whether a file of this kind is a database of tables:149+// importing picks one (see Tables), and a download writes the sheet or150+// the selection as a named table (ExportOptions.Table).151+func (k Kind) HasTables() bool { return k.format().tables }@@ -0,0 +1,37 @@
1+package fileio2+3+import "testing"4+5+// TestFormats checks every row of the formats table is complete, since6+// the UI's lists and Import and Export all come from it.7+func TestFormats(t *testing.T) {8+ for i, k := range Kinds() {9+ f := k.format()10+ if f.kind != k || k != Kind(i+1) {11+ t.Errorf("row %d is %v, want Kind order", i, f.kind)12+ }13+ if f.name == "" || f.noun == "" || f.label == "" || len(f.exts) == 0 || f.read == nil {14+ t.Errorf("%v: incomplete %+v", k, f)15+ }16+ for _, e := range f.exts {17+ if got, ok := KindOf("x" + e); !ok || got != k {18+ t.Errorf("KindOf(x%s) = %v, %v", e, got, ok)19+ }20+ }21+ if k.CanExport() != (k.MenuTitle() != "") || k.CanExport() != (k.About() != "") {22+ t.Errorf("%v: export %v, menu %q, about %q", k, k.CanExport(), k.MenuTitle(), k.About())23+ }24+ }25+ if XLSX.MenuTitle() != "Microsoft Excel (.xlsx)" || !XLSX.HoldsSheets() || CSV.HoldsSheets() {26+ t.Errorf("XLSX: %q, holds sheets %v", XLSX.MenuTitle(), XLSX.HoldsSheets())27+ }28+ if !SQLite.HasTables() || XLSX.HasTables() {29+ t.Errorf("only SQLite has tables")30+ }31+ if k := Kind(99); k.String() != "unknown" || k.Ext() != "" || k.CanExport() {32+ t.Errorf("unknown kind %q %q %v", k, k.Ext(), k.CanExport())33+ }34+ if _, ok := KindOf("notes.txt"); ok {35+ t.Errorf("KindOf(.txt) recognized")36+ }37+}