mirror of
https://github.com/larksuite/cli.git
synced 2026-07-05 15:47:54 +08:00
168 lines
4.6 KiB
Go
168 lines
4.6 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package output
|
|
|
|
import (
|
|
"sort"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
const maxFlattenDepth = 3
|
|
|
|
type flatEntry struct {
|
|
Key string
|
|
Value string
|
|
}
|
|
|
|
// flattenObject flattens a nested object into dot-notation key-value pairs.
|
|
// Objects nested beyond maxFlattenDepth levels are serialized as JSON strings.
|
|
// Keys are sorted alphabetically for deterministic column order.
|
|
func flattenObject(obj map[string]interface{}, prefix string, depth int) []flatEntry {
|
|
keys := make([]string, 0, len(obj))
|
|
for k := range obj {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
var entries []flatEntry
|
|
for _, k := range keys {
|
|
v := obj[k]
|
|
key := k
|
|
if prefix != "" {
|
|
key = prefix + "." + k
|
|
}
|
|
switch val := v.(type) {
|
|
case map[string]interface{}:
|
|
if depth+1 >= maxFlattenDepth {
|
|
entries = append(entries, flatEntry{Key: key, Value: cellStr(val)})
|
|
} else {
|
|
entries = append(entries, flattenObject(val, key, depth+1)...)
|
|
}
|
|
default:
|
|
entries = append(entries, flatEntry{Key: key, Value: cellStr(v)})
|
|
}
|
|
}
|
|
return entries
|
|
}
|
|
|
|
// collectColumns collects column names from all rows (union set),
|
|
// preserving first-occurrence order.
|
|
func collectColumns(rows [][]flatEntry) []string {
|
|
seen := map[string]bool{}
|
|
var cols []string
|
|
for _, row := range rows {
|
|
for _, e := range row {
|
|
if !seen[e.Key] {
|
|
seen[e.Key] = true
|
|
cols = append(cols, e.Key)
|
|
}
|
|
}
|
|
}
|
|
return cols
|
|
}
|
|
|
|
// rowMap converts a slice of flatEntry into a map for column lookup.
|
|
func rowMap(entries []flatEntry) map[string]string {
|
|
m := make(map[string]string, len(entries))
|
|
for _, e := range entries {
|
|
m[e.Key] = e.Value
|
|
}
|
|
return m
|
|
}
|
|
|
|
// runeWidth returns the display width of a rune.
|
|
// CJK characters and some symbols are double-width.
|
|
func runeWidth(r rune) int {
|
|
if r == utf8.RuneError {
|
|
return 1
|
|
}
|
|
// CJK Unified Ideographs, CJK Compatibility Ideographs, etc.
|
|
if (r >= 0x1100 && r <= 0x115F) || // Hangul Jamo
|
|
r == 0x2329 || r == 0x232A ||
|
|
(r >= 0x2E80 && r <= 0x303E) || // CJK Radicals, Kangxi, CJK Symbols
|
|
(r >= 0x3040 && r <= 0x33BF) || // Hiragana, Katakana, Bopomofo, etc.
|
|
(r >= 0x3400 && r <= 0x4DBF) || // CJK Unified Ideographs Extension A
|
|
(r >= 0x4E00 && r <= 0xA4CF) || // CJK Unified Ideographs, Yi
|
|
(r >= 0xA960 && r <= 0xA97C) || // Hangul Jamo Extended-A
|
|
(r >= 0xAC00 && r <= 0xD7A3) || // Hangul Syllables
|
|
(r >= 0xF900 && r <= 0xFAFF) || // CJK Compatibility Ideographs
|
|
(r >= 0xFE10 && r <= 0xFE6F) || // CJK Compatibility Forms, Small Forms
|
|
(r >= 0xFF01 && r <= 0xFF60) || // Fullwidth Forms
|
|
(r >= 0xFFE0 && r <= 0xFFE6) || // Fullwidth Signs
|
|
(r >= 0x1F300 && r <= 0x1F9FF) || // Emoji (Miscellaneous Symbols and Pictographs, Emoticons, etc.)
|
|
(r >= 0x20000 && r <= 0x2FFFF) || // CJK Unified Ideographs Extension B-F
|
|
(r >= 0x30000 && r <= 0x3FFFF) { // CJK Unified Ideographs Extension G+
|
|
return 2
|
|
}
|
|
return 1
|
|
}
|
|
|
|
// stringWidth returns the display width of a string.
|
|
func stringWidth(s string) int {
|
|
w := 0
|
|
for _, r := range s {
|
|
w += runeWidth(r)
|
|
}
|
|
return w
|
|
}
|
|
|
|
// truncateToWidth truncates a string to fit within maxWidth display columns.
|
|
// If truncated, appends "…".
|
|
func truncateToWidth(s string, maxWidth int) string {
|
|
if maxWidth <= 0 {
|
|
return ""
|
|
}
|
|
w := 0
|
|
for i, r := range s {
|
|
rw := runeWidth(r)
|
|
if w+rw > maxWidth {
|
|
return s[:i] + "…"
|
|
}
|
|
w += rw
|
|
}
|
|
return s
|
|
}
|
|
|
|
// flattenItem flattens a single item (object or other) into flatEntry pairs.
|
|
func flattenItem(item interface{}) []flatEntry {
|
|
if obj, ok := item.(map[string]interface{}); ok {
|
|
return flattenObject(obj, "", 0)
|
|
}
|
|
return []flatEntry{{Key: "value", Value: cellStr(item)}}
|
|
}
|
|
|
|
// prepareRows converts a data value into flattened rows and column names.
|
|
// Returns rows (as maps), columns, and whether the data was a list.
|
|
func prepareRows(data interface{}) (rows []map[string]string, cols []string, isList bool) {
|
|
items := extractArray(data)
|
|
if items == nil {
|
|
// Single object
|
|
if obj, ok := data.(map[string]interface{}); ok {
|
|
entries := flattenObject(obj, "", 0)
|
|
rm := rowMap(entries)
|
|
flatRows := [][]flatEntry{entries}
|
|
return []map[string]string{rm}, collectColumns(flatRows), false
|
|
}
|
|
return nil, nil, false
|
|
}
|
|
|
|
isList = true
|
|
var flatRows [][]flatEntry
|
|
for _, item := range items {
|
|
entries := flattenItem(item)
|
|
flatRows = append(flatRows, entries)
|
|
rows = append(rows, rowMap(entries))
|
|
}
|
|
cols = collectColumns(flatRows)
|
|
return rows, cols, isList
|
|
}
|
|
|
|
// extractArray extracts an array from data, or returns nil.
|
|
func extractArray(data interface{}) []interface{} {
|
|
if arr, ok := data.([]interface{}); ok {
|
|
return arr
|
|
}
|
|
return nil
|
|
}
|