1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
|
const isNumber = require('lodash/isNumber')
class InfoValidationError extends Error {
constructor (errors) {
super()
this.name = 'InfoValidationError'
this.errors = errors
}
}
function renderTable (layout, layer, opts={}) {
const {
useQuotes = false,
linePrefix = '',
columnSeparator = ','
} = opts
const minWidth = useQuotes ? 9 : 7
const table = layer.reduce((map, code, i) => {
// TODO: this would be better as a loop over `layout`, checking for a
// matching element in the `layer` array. Or, alternatively, an earlier
// validation that asserts each layer is equal in length to the number of
// keys in the layout.
if (layout[i]) {
const { row = 0, col } = layout[i]
map[row] = map[row] || []
map[row][col || map[row].length] = code
}
return map
}, [])
const columns = Math.max(...table.map(row => row.length))
const columnIndices = '.'.repeat(columns-1).split('.').map((_, i) => i)
const columnWidths = columnIndices.map(i => Math.max(
...table.map(row => (
(row[i] || []).length
+ columnSeparator.length
+ (useQuotes ? 2 : 0) // wrapping with quotes adds 2 characters
))
))
return table.map((row, rowIndex) => {
const isLastRow = rowIndex === table.length - 1
return linePrefix + columnIndices.map(i => {
const noMoreValues = row.slice(i).every(col => col === undefined)
const noFollowingValues = row.slice(i+1).every(col => col === undefined)
const padding = Math.max(minWidth, columnWidths[i])
if (noMoreValues) return ''
if (!row[i]) return ' '.repeat(padding + 1)
const column = (useQuotes ? `"${row[i]}"` : row[i]).padStart(padding)
const suffix = (isLastRow && noFollowingValues) ? '' : columnSeparator
return column + suffix
}).join('').replace(/\s+$/, '')
}).join('\n')
}
function validateInfoJson(info) {
const errors = []
if (typeof info !== 'object' || info === null) {
errors.push('info.json root must be an object')
} else if (!info.layouts) {
errors.push('info must define "layouts"')
} else if (typeof info.layouts !== 'object' || info.layouts === null) {
errors.push('layouts must be an object')
} else if (Object.values(info.layouts).length === 0) {
errors.push('layouts must define at least one layout')
} else {
for (let name in info.layouts) {
const layout = info.layouts[name]
if (typeof layout !== 'object' || layout === null) {
errors.push(`layout ${name} must be an object`)
} else if (!Array.isArray(layout.layout)) {
errors.push(`layout ${name} must define "layout" array`)
} else {
const anyKeyHasPosition = layout.layout.some(key => (
key?.row !== undefined ||
key?.col !== undefined
))
for (let i in layout.layout) {
const key = layout.layout[i]
const keyPath = `layouts[${name}].layout[${i}]`
if (typeof key !== 'object' || key === null) {
errors.push(`Key definition at ${keyPath} must be an object`)
} else {
const optionalNumberProps = ['u', 'h', 'r', 'rx', 'ry']
if (!isNumber(key.x)) {
errors.push(`Key definition at ${keyPath} must include "x" position`)
}
if (!isNumber(key.y)) {
errors.push(`Key definition at ${keyPath} must include "y" position`)
}
for (let prop of optionalNumberProps) {
if (prop in key && !isNumber(key[prop])) {
errors.push(`Key definition at ${keyPath} optional "${prop}" must be number`)
}
}
for (let prop of ['row', 'col']) {
if (anyKeyHasPosition && !(prop in key)) {
errors.push(`Key definition at ${keyPath} is missing "${prop}"`)
} else if (prop in key && (!Number.isInteger(key[prop]) || key[prop] < 0)) {
errors.push(`Key definition at ${keyPath} "${prop}" must be a non-negative integer`)
}
}
}
}
}
}
}
if (errors.length) {
throw new InfoValidationError(errors)
}
}
module.exports = {
InfoValidationError,
renderTable,
validateInfoJson
}
|