@changfenhuang/dsh-genui 0.11.3 → 0.11.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,151 @@
1
+ import { GENUI_LIMITS } from './genui-runtime/index.ts'
2
+
3
+ /** 保存 repair 所需的原始 table 字段。 */
4
+ export interface TableDetailSource {
5
+ columns?: unknown
6
+ rows?: unknown
7
+ data?: unknown
8
+ types?: unknown
9
+ }
10
+
11
+ /** 保存 repair 后的 table 列和行。 */
12
+ export interface PreparedTableRows {
13
+ columns: string[]
14
+ rows: Array<Array<string | number>>
15
+ }
16
+
17
+ /** 使用 table repair 相同的转换规则处理列和行。 */
18
+ export function prepareTableRows(table: TableDetailSource): PreparedTableRows | null {
19
+ const declaredColumns = table.columns
20
+ const columnsSpecified = Array.isArray(declaredColumns) && declaredColumns.length > 0
21
+ const rawColumns = columnsSpecified && Array.isArray(declaredColumns)
22
+ && typeof declaredColumns[0] === 'object' && declaredColumns[0] !== null
23
+ ? declaredColumns.map(columnHeaderText)
24
+ : declaredColumns
25
+ let columns = repairColumnValues(rawColumns)
26
+ let rawRows = table.rows !== undefined ? table.rows : table.data
27
+
28
+ if (Array.isArray(rawRows) && rawRows.length > 0
29
+ && typeof rawRows[0] === 'object' && rawRows[0] !== null && !Array.isArray(rawRows[0])) {
30
+ const keys = Array.isArray(table.columns) && table.columns.length > 0
31
+ && typeof table.columns[0] === 'object' && table.columns[0] !== null
32
+ ? table.columns.map(columnKeyOf).filter((key): key is string => key !== undefined)
33
+ : Object.keys(rawRows[0] as Record<string, unknown>)
34
+ rawRows = rawRows.map(row => keys.map(key => cellText((row as Record<string, unknown>)[key])))
35
+ }
36
+
37
+ let derived: PreparedTableRows | null = null
38
+ if (!columnsSpecified && Array.isArray(rawRows) && rawRows.length > 0 && Array.isArray(rawRows[0])) {
39
+ const grid = repairRows(rawRows)
40
+ const candidate = grid.length === 0 ? null : deriveTableColumns(grid)
41
+ if (candidate !== null && repairRows(candidate.rows).length === candidate.rows.length) {
42
+ derived = candidate
43
+ columns = candidate.columns.map(column => column.slice(0, 128))
44
+ }
45
+ }
46
+
47
+ if (columns.length === 0 && !Array.isArray(table.columns)) return null
48
+ const rows = repairRows(derived === null ? rawRows : derived.rows)
49
+ if (!Array.isArray(rawRows)) return null
50
+ return { columns: columns.slice(0, GENUI_LIMITS.maxTableCols), rows }
51
+ }
52
+
53
+ /** 返回 repair 后可显示详情的 table 行。 */
54
+ export function tableRowsForDetails<Row = unknown>(table: TableDetailSource): Row[] {
55
+ return (prepareTableRows(table)?.rows ?? []) as Row[]
56
+ }
57
+
58
+ /** 按照 table renderer 的规则识别分组标题行。 */
59
+ export function isTableGroupHeaderRow(row: unknown, types: unknown): boolean {
60
+ if (!Array.isArray(row) || !Array.isArray(types) || types[0] !== 'group') return false
61
+ return String(row[0] ?? '').trim() !== ''
62
+ && row.slice(1).every(cell => String(cell ?? '').trim() === '')
63
+ }
64
+
65
+ /** 判断详情索引对应的行是否可展开并显示。 */
66
+ export function isTableDetailReachable(table: TableDetailSource, rowIndex: number): boolean {
67
+ const rows = tableRowsForDetails(table)
68
+ return rowIndex >= 0
69
+ && rowIndex < rows.length
70
+ && !isTableGroupHeaderRow(rows[rowIndex], table.types)
71
+ }
72
+
73
+ /** 按照 guard 接受的 object alias 规则修复 table 列。 */
74
+ function repairColumnValues(value: unknown): string[] {
75
+ if (!Array.isArray(value)) return []
76
+ const columns: string[] = []
77
+ for (const item of value) {
78
+ if (columns.length >= GENUI_LIMITS.maxTableCols) break
79
+ if (typeof item === 'string') columns.push(item.slice(0, 128))
80
+ else if (item !== null && typeof item === 'object') {
81
+ const column = item as Record<string, unknown>
82
+ const label = typeof column.label === 'string' ? column.label
83
+ : typeof column.value === 'string' ? column.value
84
+ : typeof column.title === 'string' ? column.title
85
+ : JSON.stringify(item)
86
+ columns.push(label.slice(0, 128))
87
+ }
88
+ }
89
+ return columns
90
+ }
91
+
92
+ /** 按照 table repair 的单元格规则裁剪并清理行。 */
93
+ function repairRows(value: unknown): Array<Array<string | number>> {
94
+ if (!Array.isArray(value)) return []
95
+ const rows: Array<Array<string | number>> = []
96
+ for (const row of value) {
97
+ if (rows.length >= GENUI_LIMITS.maxTableRows) break
98
+ if (!Array.isArray(row)) continue
99
+ const cells: Array<string | number> = []
100
+ for (const cell of row) {
101
+ if (cells.length >= GENUI_LIMITS.maxTableCols) break
102
+ if (typeof cell === 'string') cells.push(cell.slice(0, 256))
103
+ else if (typeof cell === 'number' && Number.isFinite(cell)) cells.push(cell)
104
+ }
105
+ if (cells.length > 0) rows.push(cells)
106
+ }
107
+ return rows
108
+ }
109
+
110
+ /** 从有效的首行表头生成 table 列。 */
111
+ function deriveTableColumns(rows: Array<Array<string | number>>): PreparedTableRows | null {
112
+ const header = rows[0]
113
+ if (header === undefined || header.length === 0) return null
114
+ const body = rows.slice(1)
115
+ if (body.length === 0) {
116
+ const columns = header.map(cell => String(cell).trim())
117
+ return columns.every(column => column !== '') ? { columns, rows: [] } : null
118
+ }
119
+ if (body.every(row => row.length === header.length)) {
120
+ return { columns: header.map(cell => String(cell).trim()), rows: body }
121
+ }
122
+ return { columns: Array.from({ length: header.length }, (_unused, index) => `列${index + 1}`), rows }
123
+ }
124
+
125
+ /** 读取 object row 映射到列时使用的 key。 */
126
+ function columnKeyOf(value: unknown): string | undefined {
127
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined
128
+ const column = value as Record<string, unknown>
129
+ for (const key of ['key', 'dataIndex', 'title', 'label'] as const) {
130
+ if (typeof column[key] === 'string' && column[key] !== '') return column[key]
131
+ }
132
+ return undefined
133
+ }
134
+
135
+ /** 将 object row 中的值转换为 table 单元格,并保留列位置。 */
136
+ function cellText(value: unknown): string | number {
137
+ if (typeof value === 'string') return value
138
+ if (typeof value === 'number' && Number.isFinite(value)) return value
139
+ if (value === null || value === undefined) return ''
140
+ return JSON.stringify(value)
141
+ }
142
+
143
+ /** 读取 object column 显示的标题。 */
144
+ function columnHeaderText(value: unknown): string {
145
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) return String(value)
146
+ const column = value as Record<string, unknown>
147
+ for (const key of ['title', 'label', 'key', 'dataIndex'] as const) {
148
+ if (typeof column[key] === 'string' && column[key] !== '') return column[key]
149
+ }
150
+ return JSON.stringify(value)
151
+ }
@@ -0,0 +1,43 @@
1
+ import type { GenuiNode, GenuiSpec } from './spec.ts'
2
+ import { GENUI_LIMITS } from './genui-runtime/index.ts'
3
+ import { isTableDetailReachable, tableRowsForDetails } from './table-details.ts'
4
+
5
+ /** 遍历完整 GenUI 组件树,并提供每个组件在 spec 中的路径。 */
6
+ export function walkGenuiNodes(spec: GenuiSpec, visitor: (node: GenuiNode, path: string) => void): void {
7
+ const walk = (nodes: GenuiNode[], path: string, depth: number): void => {
8
+ if (depth > GENUI_LIMITS.maxDepth) return
9
+ nodes.forEach((node, index) => visit(node, `${path}[${index}]`, depth))
10
+ }
11
+ const visit = (node: GenuiNode, at: string, depth: number): void => {
12
+ if (depth > GENUI_LIMITS.maxDepth) return
13
+ visitor(node, at)
14
+ if (depth >= GENUI_LIMITS.maxDepth) return
15
+ switch (node.type) {
16
+ case 'row':
17
+ case 'col':
18
+ case 'grid':
19
+ case 'card':
20
+ walk(node.items, `${at}.items`, depth + 1)
21
+ break
22
+ case 'tabs':
23
+ node.tabs.forEach((tab, tabIndex) => walk(tab.items, `${at}.tabs[${tabIndex}].items`, depth + 1))
24
+ break
25
+ case 'accordion':
26
+ node.items.forEach((item, itemIndex) => walk(item.items, `${at}.items[${itemIndex}].items`, depth + 1))
27
+ break
28
+ case 'list':
29
+ node.items.forEach((item, itemIndex) => {
30
+ if (item !== null && typeof item === 'object' && 'type' in item) visit(item, `${at}.items[${itemIndex}]`, depth + 1)
31
+ })
32
+ break
33
+ case 'table':
34
+ node.details?.slice(0, tableRowsForDetails(node).length).forEach((detail, rowIndex) => {
35
+ if (detail !== null && isTableDetailReachable(node, rowIndex)) {
36
+ walk(detail, `${at}.details[${rowIndex}]`, depth + 1)
37
+ }
38
+ })
39
+ break
40
+ }
41
+ }
42
+ walk(spec.items, 'items', 0)
43
+ }