Metadata: Report supported Exiftool, XMP, and Dublin Core tags #2252

Replaces the --no-wrap flag with --md in all "photoprism show ..."
subcommands, as this is easier to understand. See also #2247.
Unused code was opportunistically removed along the way.
This commit is contained in:
Michael Mayer 2022-04-14 10:49:56 +02:00
parent b3113e006f
commit 0096243240
20 changed files with 274 additions and 176 deletions

View file

@ -63,6 +63,7 @@ var PhotoPrism = []cli.Command{
UsersCommand,
ShowCommand,
VersionCommand,
ShowConfigCommand,
}
// childAlreadyRunning tests if a .pid file at filePath is a running process.

View file

@ -10,6 +10,7 @@ var ShowCommand = cli.Command{
Usage: "Configuration and system report subcommands",
Subcommands: []cli.Command{
ShowConfigCommand,
ShowTagsCommand,
ShowFiltersCommand,
ShowFormatsCommand,
},

View file

@ -12,11 +12,11 @@ import (
var ShowConfigCommand = cli.Command{
Name: "config",
Usage: "Shows global configuration values",
Usage: "Shows global config option names and values",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "no-wrap, n",
Usage: "disable text-wrapping",
Name: "md, m",
Usage: "renders valid Markdown",
},
},
Action: showConfigAction,
@ -27,9 +27,9 @@ func showConfigAction(ctx *cli.Context) error {
conf := config.NewConfig(ctx)
conf.SetLogLevel(logrus.FatalLevel)
rows, cols := conf.Table()
rows, cols := conf.Report()
fmt.Println(report.Markdown(rows, cols, !ctx.Bool("no-wrap")))
fmt.Println(report.Table(rows, cols, ctx.Bool("md")))
return nil
}

View file

@ -15,8 +15,8 @@ var ShowFiltersCommand = cli.Command{
Usage: "Displays a search filter overview with examples",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "no-wrap, n",
Usage: "disable text-wrapping so the output can be pasted into Markdown files",
Name: "md, m",
Usage: "renders valid Markdown",
},
},
Action: showFiltersAction,
@ -24,7 +24,7 @@ var ShowFiltersCommand = cli.Command{
// showFiltersAction lists supported search filters.
func showFiltersAction(ctx *cli.Context) error {
rows, cols := form.Table(&form.SearchPhotos{})
rows, cols := form.Report(&form.SearchPhotos{})
sort.Slice(rows, func(i, j int) bool {
if rows[i][1] == rows[j][1] {
@ -34,7 +34,7 @@ func showFiltersAction(ctx *cli.Context) error {
}
})
fmt.Println(report.Markdown(rows, cols, !ctx.Bool("no-wrap")))
fmt.Println(report.Table(rows, cols, ctx.Bool("md")))
return nil
}

View file

@ -18,8 +18,8 @@ var ShowFormatsCommand = cli.Command{
Usage: "hide format descriptions to make the output more compact",
},
cli.BoolFlag{
Name: "no-wrap, n",
Usage: "disable text-wrapping so the output can be pasted into Markdown files",
Name: "md, m",
Usage: "renders valid Markdown",
},
},
Action: showFormatsAction,
@ -27,9 +27,9 @@ var ShowFormatsCommand = cli.Command{
// showFormatsAction lists supported media and sidecar file formats.
func showFormatsAction(ctx *cli.Context) error {
rows, cols := fs.Extensions.Formats(true).Table(!ctx.Bool("compact"), true, true)
rows, cols := fs.Extensions.Formats(true).Report(!ctx.Bool("compact"), true, true)
fmt.Println(report.Markdown(rows, cols, !ctx.Bool("no-wrap")))
fmt.Println(report.Table(rows, cols, ctx.Bool("md")))
return nil
}

View file

@ -0,0 +1,41 @@
package commands
import (
"fmt"
"sort"
"github.com/photoprism/photoprism/internal/meta"
"github.com/urfave/cli"
"github.com/photoprism/photoprism/pkg/report"
)
var ShowTagsCommand = cli.Command{
Name: "tags",
Usage: "Reports supported Exif and XMP metadata tags",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "md, m",
Usage: "renders valid Markdown",
},
},
Action: showTagsAction,
}
// showTagsAction reports supported Exif and XMP metadata tags.
func showTagsAction(ctx *cli.Context) error {
rows, cols := meta.Report(&meta.Data{})
sort.Slice(rows, func(i, j int) bool {
if rows[i][1] == rows[j][1] {
return rows[i][0] < rows[j][0]
} else {
return rows[i][1] < rows[j][1]
}
})
fmt.Println(report.Table(rows, cols, ctx.Bool("md")))
return nil
}

View file

@ -1,17 +1,14 @@
package config
import (
"bytes"
"fmt"
"strings"
"time"
"unicode/utf8"
"github.com/olekukonko/tablewriter"
)
// Table returns global config values as a table for reporting.
func (c *Config) Table() (rows [][]string, cols []string) {
// Report returns global config values as a table for reporting.
func (c *Config) Report() (rows [][]string, cols []string) {
cols = []string{"Value", "Name"}
rows = [][]string{
@ -166,22 +163,3 @@ func (c *Config) Table() (rows [][]string, cols []string) {
return rows, cols
}
// MarkdownTable returns global config values as a markdown formatted table.
func (c *Config) MarkdownTable(autoWrap bool) string {
buf := &bytes.Buffer{}
rows, cols := c.Table()
table := tablewriter.NewWriter(buf)
table.SetAutoWrapText(autoWrap)
table.SetAutoFormatHeaders(false)
table.SetHeader(cols)
table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false})
table.SetCenterSeparator("|")
table.AppendBulk(rows)
table.Render()
return buf.String()
}

View file

@ -544,13 +544,18 @@ func (m *File) Links() Links {
return FindLinks("", m.FileUID)
}
// Panorama tests if the file seems to be a panorama image.
// Panorama checks if the file appears to be a panoramic image.
func (m *File) Panorama() bool {
if m.FileSidecar || m.FileWidth <= 1000 || m.FileHeight <= 500 {
// Too small.
return false
} else if m.Projection() != ProjDefault {
// Panoramic projection.
return true
}
return m.Projection() != ProjDefault || (m.FileWidth/m.FileHeight) >= 2
// Decide based on aspect ratio.
return float64(m.FileWidth)/float64(m.FileHeight) > 1.9
}
// Projection returns the panorama projection name if any.

View file

@ -278,8 +278,8 @@ func TestFile_Panorama(t *testing.T) {
assert.True(t, file.Panorama())
})
t.Run("1999", func(t *testing.T) {
file := &File{Photo: nil, FileType: "jpg", FileSidecar: false, FileWidth: 1999, FileHeight: 1000}
assert.False(t, file.Panorama())
file := &File{Photo: nil, FileType: "jpg", FileSidecar: false, FileWidth: 1910, FileHeight: 1000}
assert.True(t, file.Panorama())
})
t.Run("2000", func(t *testing.T) {
file := &File{Photo: nil, FileType: "jpg", FileSidecar: false, FileWidth: 2000, FileHeight: 1000}

View file

@ -8,8 +8,8 @@ import (
"github.com/photoprism/photoprism/pkg/sanitize"
)
// Table returns form fields as table rows for reports.
func Table(f interface{}) (rows [][]string, cols []string) {
// Report returns form fields as table rows for reports.
func Report(f interface{}) (rows [][]string, cols []string) {
cols = []string{"Filter", "Type", "Examples", "Notes"}
v := reflect.ValueOf(f)
@ -26,6 +26,9 @@ func Table(f interface{}) (rows [][]string, cols []string) {
// Iterate through all form fields.
for i := 0; i < v.NumField(); i++ {
if !v.Type().Field(i).IsExported() {
continue
}
fieldValue := v.Field(i)
fieldName := v.Type().Field(i).Tag.Get("form")
fieldInfo := v.Type().Field(i).Tag.Get("serialize")
@ -68,7 +71,7 @@ func Table(f interface{}) (rows [][]string, cols []string) {
example = fmt.Sprintf("%s:yes %s:no", fieldName, fieldName)
}
default:
log.Warnf("failed exporting %T %s", t, sanitize.Token(fieldName))
log.Warnf("failed reporting on %T %s", t, sanitize.Token(fieldName))
continue
}

View file

@ -22,13 +22,13 @@ type SearchPhotos struct {
Unstacked bool `form:"unstacked"`
Stackable bool `form:"stackable"`
Video bool `form:"video"`
Vector bool `form:"vector" notes:"Vector graphics such as SVGs"`
Animated bool `form:"animated" notes:"Animated images such as GIFs"`
Photo bool `form:"photo" notes:"Everything except videos"`
Raw bool `form:"raw" notes:"RAW images only"`
Live bool `form:"live" notes:"Live photos, short videos"`
Scan bool `form:"scan" notes:"Scanned images and documents"`
Panorama bool `form:"panorama" notes:"Aspect ratio 2:1 and up"`
Vector bool `form:"vector" notes:"Vector Graphics"`
Animated bool `form:"animated" notes:"Animated GIFs"`
Photo bool `form:"photo" notes:"No Videos"`
Raw bool `form:"raw" notes:"RAW Images"`
Live bool `form:"live" notes:"Live Photos, Short Videos"`
Scan bool `form:"scan" notes:"Scanned Images, Documents"`
Panorama bool `form:"panorama" notes:"Aspect Ratio > 1.9:1"`
Portrait bool `form:"portrait"`
Landscape bool `form:"landscape"`
Square bool `form:"square"`
@ -48,33 +48,33 @@ type SearchPhotos struct {
Diff uint32 `form:"diff"`
Mono bool `form:"mono"`
Geo bool `form:"geo"`
Keywords string `form:"keywords"` // Filter by keyword(s)
Label string `form:"label"` // Label name
Category string `form:"category"` // Moments
Country string `form:"country"` // Moments
State string `form:"state"` // Moments
Year string `form:"year"` // Moments
Month string `form:"month"` // Moments
Day string `form:"day"` // Moments
Face string `form:"face"` // UIDs
Subject string `form:"subject"` // UIDs
Person string `form:"person"` // Alias for Subject
Subjects string `form:"subjects"` // People names
People string `form:"people"` // Alias for Subjects
Album string `form:"album" notes:"Single name with * wildcard"` // Album UIDs or name
Albums string `form:"albums" example:"albums:\"South Africa & Birds\"" notes:"May be combined with & and |"` // Multi search with and/or
Color string `form:"color"` // Main color
Faces string `form:"faces"` // Find or exclude faces if detected.
Quality int `form:"quality"` // Photo quality score
Review bool `form:"review"` // Find photos in review
Camera string `form:"camera" example:"camera:canon"` // Camera UID or name
Lens string `form:"lens" example:"lens:ef24"` // Lens UID or name
Before time.Time `form:"before" time_format:"2006-01-02" notes:"Taken before this date"` // Finds images taken before date
After time.Time `form:"after" time_format:"2006-01-02" notes:"Taken after this date"` // Finds images taken after date
Count int `form:"count" binding:"required" serialize:"-"` // Result FILE limit
Offset int `form:"offset" serialize:"-"` // Result FILE offset
Order string `form:"order" serialize:"-"` // Sort order
Merged bool `form:"merged" serialize:"-"` // Merge FILES in response
Keywords string `form:"keywords"` // Filter by keyword(s)
Label string `form:"label"` // Label name
Category string `form:"category"` // Moments
Country string `form:"country"` // Moments
State string `form:"state"` // Moments
Year string `form:"year"` // Moments
Month string `form:"month"` // Moments
Day string `form:"day"` // Moments
Face string `form:"face"` // UIDs
Subject string `form:"subject"` // UIDs
Person string `form:"person"` // Alias for Subject
Subjects string `form:"subjects"` // People names
People string `form:"people"` // Alias for Subjects
Album string `form:"album" notes:"Single name with * wildcard"` // Album UIDs or name
Albums string `form:"albums" example:"albums:\"South Africa & Birds\"" notes:"Album names can be combined with & and |"` // Multi search with and/or
Color string `form:"color"` // Main color
Faces string `form:"faces"` // Find or exclude faces if detected.
Quality int `form:"quality"` // Photo quality score
Review bool `form:"review"` // Find photos in review
Camera string `form:"camera" example:"camera:canon"` // Camera UID or name
Lens string `form:"lens" example:"lens:ef24"` // Lens UID or name
Before time.Time `form:"before" time_format:"2006-01-02" notes:"Taken before this date"` // Finds images taken before date
After time.Time `form:"after" time_format:"2006-01-02" notes:"Taken after this date"` // Finds images taken after date
Count int `form:"count" binding:"required" serialize:"-"` // Result FILE limit
Offset int `form:"offset" serialize:"-"` // Result FILE offset
Order string `form:"order" serialize:"-"` // Sort order
Merged bool `form:"merged" serialize:"-"` // Merge FILES in response
}
func (f *SearchPhotos) GetQuery() string {

View file

@ -17,7 +17,7 @@ type Data struct {
FileName string `meta:"FileName"`
DocumentID string `meta:"BurstUUID,MediaGroupUUID,ImageUniqueID,OriginalDocumentID,DocumentID"`
InstanceID string `meta:"InstanceID,DocumentID"`
TakenAt time.Time `meta:"DateTimeOriginal,CreationDate,CreateDate,MediaCreateDate,ContentCreateDate,DateTimeDigitized,DateTime,SubSecDateTimeOriginal,SubSecCreateDate"`
TakenAt time.Time `meta:"DateTimeOriginal,CreationDate,CreateDate,MediaCreateDate,ContentCreateDate,DateTimeDigitized,DateTime,SubSecDateTimeOriginal,SubSecCreateDate" xmp:"DateCreated"`
TakenAtLocal time.Time `meta:"DateTimeOriginal,CreationDate,CreateDate,MediaCreateDate,ContentCreateDate,DateTimeDigitized,DateTime,SubSecDateTimeOriginal,SubSecCreateDate"`
TakenGps time.Time `meta:"GPSDateTime,GPSDateStamp"`
TakenNs int `meta:"-"`
@ -26,27 +26,27 @@ type Data struct {
FPS float64 `meta:"VideoFrameRate,VideoAvgFrameRate"`
Frames int `meta:"FrameCount"`
Codec string `meta:"CompressorID,FileType"`
Title string `meta:"Title"`
Subject string `meta:"Subject,PersonInImage,ObjectName,HierarchicalSubject,CatalogSets"`
Title string `meta:"Title" xmp:"dc:title" dc:"title,title.Alt"`
Subject string `meta:"Subject,PersonInImage,ObjectName,HierarchicalSubject,CatalogSets" xmp:"Subject"`
Keywords Keywords `meta:"Keywords"`
Notes string `meta:"Comment"`
Artist string `meta:"Artist,Creator,OwnerName,Owner"`
Description string `meta:"Description"`
Copyright string `meta:"Rights,Copyright,WebStatement,Certificate"`
Artist string `meta:"Artist,Creator,OwnerName,Owner" xmp:"Creator"`
Description string `meta:"Description" xmp:"Description,Description.Alt"`
Copyright string `meta:"Rights,Copyright,WebStatement" xmp:"Rights,Rights.Alt"`
License string `meta:"UsageTerms,License"`
Projection string `meta:"ProjectionType"`
ColorProfile string `meta:"ICCProfileName,ProfileDescription"`
CameraMake string `meta:"CameraMake,Make"`
CameraModel string `meta:"CameraModel,Model"`
CameraMake string `meta:"CameraMake,Make" xmp:"Make"`
CameraModel string `meta:"CameraModel,Model" xmp:"Model"`
CameraOwner string `meta:"OwnerName"`
CameraSerial string `meta:"SerialNumber"`
LensMake string `meta:"LensMake"`
LensModel string `meta:"Lens,LensModel"`
LensModel string `meta:"Lens,LensModel" xmp:"LensModel"`
Software string `meta:"Software,HistorySoftwareAgent,ProcessingSoftware"`
Flash bool `meta:"-"`
Flash bool `meta:"FlashFired"`
FocalLength int `meta:"FocalLength"`
Exposure string `meta:"ExposureTime"`
Aperture float32 `meta:"ApertureValue"`
Exposure string `meta:"ExposureTime,ShutterSpeedValue,ShutterSpeed,TargetExposureTime"`
Aperture float32 `meta:"ApertureValue,Aperture"`
FNumber float32 `meta:"FNumber"`
Iso int `meta:"ISO"`
ImageType int `meta:"HDRImageType"`
@ -73,16 +73,14 @@ func NewData() Data {
// AspectRatio returns the aspect ratio based on width and height.
func (data Data) AspectRatio() float32 {
width := float64(data.ActualWidth())
height := float64(data.ActualHeight())
w := float64(data.ActualWidth())
h := float64(data.ActualHeight())
if width <= 0 || height <= 0 {
if w <= 0 || h <= 0 {
return 0
}
aspectRatio := float32(math.Round((width/height)*100) / 100)
return aspectRatio
return float32(math.Round((w/h)*100) / 100)
}
// Portrait returns true if it is a portrait picture or video based on width and height.

View file

@ -4,6 +4,8 @@ import (
"regexp"
"strconv"
"github.com/photoprism/photoprism/pkg/sanitize"
"github.com/dsoprea/go-exif/v3"
)
@ -13,18 +15,21 @@ var GpsFloatRegexp = regexp.MustCompile("[+\\-]?(?:(?:0|[1-9]\\d*)(?:\\.\\d*)?|\
// GpsToLatLng returns the GPS latitude and longitude as float point number.
func GpsToLatLng(s string) (lat, lng float32) {
// Emtpy?
if s == "" {
return 0, 0
}
// Floating point numbers?
if fl := GpsFloatRegexp.FindAllString(s, -1); len(fl) == 2 {
lat, _ := strconv.ParseFloat(fl[0], 64)
lng, _ := strconv.ParseFloat(fl[1], 64)
return float32(lat), float32(lng)
if lat, err := strconv.ParseFloat(fl[0], 64); err != nil {
log.Infof("metadata: %s is not a valid gps position", sanitize.Log(fl[0]))
} else if lng, err := strconv.ParseFloat(fl[1], 64); err == nil {
return float32(lat), float32(lng)
}
}
// Parse human readable strings.
// Parse string values.
co := GpsCoordsRegexp.FindAllString(s, -1)
re := GpsRefRegexp.FindAllString(s, -1)
@ -34,16 +39,16 @@ func GpsToLatLng(s string) (lat, lng float32) {
latDeg := exif.GpsDegrees{
Orientation: re[0][0],
Degrees: GpsCoord(co[0]),
Minutes: GpsCoord(co[1]),
Seconds: GpsCoord(co[2]),
Degrees: ParseFloat(co[0]),
Minutes: ParseFloat(co[1]),
Seconds: ParseFloat(co[2]),
}
lngDeg := exif.GpsDegrees{
Orientation: re[1][0],
Degrees: GpsCoord(co[3]),
Minutes: GpsCoord(co[4]),
Seconds: GpsCoord(co[5]),
Degrees: ParseFloat(co[3]),
Minutes: ParseFloat(co[4]),
Seconds: ParseFloat(co[5]),
}
return float32(latDeg.Decimal()), float32(lngDeg.Decimal())
@ -51,10 +56,17 @@ func GpsToLatLng(s string) (lat, lng float32) {
// GpsToDecimal returns the GPS latitude or longitude as decimal float point number.
func GpsToDecimal(s string) float32 {
// Emtpy?
if s == "" {
return 0
}
// Floating point number?
if f, err := strconv.ParseFloat(s, 64); err == nil {
return float32(f)
}
// Parse string value.
co := GpsCoordsRegexp.FindAllString(s, -1)
re := GpsRefRegexp.FindAllString(s, -1)
@ -64,26 +76,26 @@ func GpsToDecimal(s string) float32 {
latDeg := exif.GpsDegrees{
Orientation: re[0][0],
Degrees: GpsCoord(co[0]),
Minutes: GpsCoord(co[1]),
Seconds: GpsCoord(co[2]),
Degrees: ParseFloat(co[0]),
Minutes: ParseFloat(co[1]),
Seconds: ParseFloat(co[2]),
}
return float32(latDeg.Decimal())
}
// GpsCoord returns a single GPS coordinate value as floating point number (degree, minute or second).
func GpsCoord(s string) float64 {
// ParseFloat returns a single GPS coordinate value as floating point number (degree, minute or second).
func ParseFloat(s string) float64 {
// Empty?
if s == "" {
return 0
}
result, err := strconv.ParseFloat(s, 64)
if err != nil {
log.Debugf("metadata: failed parsing GPS coordinate '%s'", s)
// Parse floating point number.
if result, err := strconv.ParseFloat(s, 64); err != nil {
log.Debugf("metadata: %s is not a valid gps position", sanitize.Log(s))
return 0
} else {
return result
}
return result
}

View file

@ -70,17 +70,17 @@ func TestGpsToDecimal(t *testing.T) {
func TestGpsCoord(t *testing.T) {
t.Run("valid string", func(t *testing.T) {
r := GpsCoord("51")
r := ParseFloat("51")
assert.Equal(t, float64(51), r)
})
t.Run("empty string", func(t *testing.T) {
r := GpsCoord("")
r := ParseFloat("")
assert.Equal(t, float64(0), r)
})
t.Run("invalid string", func(t *testing.T) {
r := GpsCoord("abc")
r := ParseFloat("abc")
assert.Equal(t, float64(0), r)
})
}

75
internal/meta/report.go Normal file
View file

@ -0,0 +1,75 @@
package meta
import (
"reflect"
"strings"
"time"
"github.com/photoprism/photoprism/pkg/sanitize"
)
// Report returns form fields as table rows for reports.
func Report(f interface{}) (rows [][]string, cols []string) {
cols = []string{"Tag", "Type", "Exiftool", "XMP", "Dublin Core"}
v := reflect.ValueOf(f)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return rows, cols
}
rows = make([][]string, 0, v.NumField())
// Iterate through all form fields.
for i := 0; i < v.NumField(); i++ {
if !v.Type().Field(i).IsExported() {
continue
}
fieldValue := v.Field(i)
fieldName := v.Type().Field(i).Name
metaTags := v.Type().Field(i).Tag.Get("meta")
xmpTags := v.Type().Field(i).Tag.Get("xmp")
dcTags := v.Type().Field(i).Tag.Get("dc")
// Serialize field values as string.
if metaTags != "" && metaTags != "-" {
typeName := "any"
switch t := fieldValue.Interface().(type) {
case Keywords:
typeName = "keywords"
case time.Duration:
typeName = "duration"
case time.Time:
typeName = "timestamp"
case int, int8, int16, int32, int64:
typeName = "number"
case uint, uint8, uint16, uint32, uint64:
typeName = "number"
case float32, float64:
typeName = "decimal"
case string:
typeName = "text"
case bool:
typeName = "flag"
default:
log.Warnf("failed reporting on %T %s", t, sanitize.Token(fieldName))
continue
}
metaTags = strings.ReplaceAll(metaTags, ",", ", ")
xmpTags = strings.ReplaceAll(xmpTags, ",", ", ")
dcTags = strings.ReplaceAll(dcTags, ",", ", ")
rows = append(rows, []string{fieldName, typeName, metaTags, xmpTags, dcTags})
}
}
return rows, cols
}

View file

@ -9,37 +9,12 @@ import (
"github.com/photoprism/photoprism/pkg/sanitize"
)
// IDs represents a list of identifier strings.
type IDs []string
// FaceMap maps identification strings to face entities.
type FaceMap map[string]entity.Face
// IDs returns all known face ids as slice.
func (m FaceMap) IDs() (ids IDs) {
ids = make(IDs, len(m))
for id := range m {
ids = append(ids, id)
}
return ids
}
// Refresh updates the map with current entity values from the database.
func (m FaceMap) Refresh() (err error) {
result := entity.Faces{}
ids := m.IDs()
if err = Db().Where("id IN (?)", ids).Find(&result).Error; err != nil {
return err
}
for _, f := range result {
m[f.ID] = f
}
return nil
}
// FacesByID retrieves faces from the database and returns a map with the Face ID as key.
func FacesByID(knownOnly, unmatchedOnly, inclHidden bool) (FaceMap, IDs, error) {
faces, err := Faces(knownOnly, unmatchedOnly, inclHidden)

View file

@ -97,8 +97,8 @@ var FormatDesc = map[Format]string{
FormatOther: "Other",
}
// Table returns a file format documentation table.
func (m FileFormats) Table(withDesc, withType, withExt bool) (rows [][]string, cols []string) {
// Report returns a file format documentation table.
func (m FileFormats) Report(withDesc, withType, withExt bool) (rows [][]string, cols []string) {
cols = make([]string, 0, 4)
cols = append(cols, "Format")

View file

@ -9,7 +9,7 @@ import (
func TestFileFormats_Markdown(t *testing.T) {
t.Run("All", func(t *testing.T) {
f := Extensions.Formats(true)
rows, cols := f.Table(true, true, true)
rows, cols := f.Report(true, true, true)
assert.NotEmpty(t, rows)
assert.NotEmpty(t, cols)
assert.Len(t, cols, 4)
@ -17,7 +17,7 @@ func TestFileFormats_Markdown(t *testing.T) {
})
t.Run("Compact", func(t *testing.T) {
f := Extensions.Formats(true)
rows, cols := f.Table(false, false, false)
rows, cols := f.Report(false, false, false)
assert.NotEmpty(t, rows)
assert.NotEmpty(t, cols)
assert.Len(t, cols, 1)

View file

@ -1,24 +0,0 @@
package report
import (
"bytes"
"github.com/olekukonko/tablewriter"
)
// Markdown returns markdown formatted table.
func Markdown(rows [][]string, cols []string, autoWrap bool) string {
buf := &bytes.Buffer{}
table := tablewriter.NewWriter(buf)
table.SetAutoWrapText(autoWrap)
table.SetAutoFormatHeaders(false)
table.SetHeader(cols)
table.SetBorders(tablewriter.Border{Left: true, Top: true, Right: true, Bottom: true})
table.SetCenterSeparator("|")
table.AppendBulk(rows)
table.Render()
return buf.String()
}

33
pkg/report/table.go Normal file
View file

@ -0,0 +1,33 @@
package report
import (
"bytes"
"github.com/olekukonko/tablewriter"
)
// Table returns a text-formatted table, optionally as valid Markdown,
// so the output can be pasted into the docs.
func Table(rows [][]string, cols []string, markDown bool) string {
buf := &bytes.Buffer{}
// Configure.
borders := tablewriter.Border{
Left: true,
Right: true,
Top: !markDown,
Bottom: !markDown,
}
// Render.
table := tablewriter.NewWriter(buf)
table.SetAutoWrapText(!markDown)
table.SetAutoFormatHeaders(false)
table.SetHeader(cols)
table.SetBorders(borders)
table.SetCenterSeparator("|")
table.AppendBulk(rows)
table.Render()
return buf.String()
}