@lewishowles/cli-style 0.9.0 → 0.10.0
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.
- package/CHANGELOG.md +13 -0
- package/README.md +77 -1
- package/bin/cli-style.js +10 -0
- package/package.json +1 -1
- package/src/capability/terminal.js +98 -0
- package/src/catalogue/renderer-catalogue.js +62 -0
- package/src/cli/catalogue-command.js +108 -0
- package/src/cli/gallery-command.js +8 -1
- package/src/cli/help.js +5 -0
- package/src/cli/render-command.js +17 -2
- package/src/formatters/ansi.js +17 -1
- package/src/gallery/render-gallery.js +108 -2
- package/src/index.js +18 -2
- package/src/patterns/diff-block.js +333 -0
- package/src/primitives/chip.js +1 -1
- package/src/primitives/panel.js +6 -1
- package/src/primitives/sparkline.js +209 -0
- package/src/theme/colours.js +172 -36
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { foreground } from "../formatters/ansi.js";
|
|
2
|
+
import { getToneColour } from "../theme/colours.js";
|
|
3
|
+
|
|
4
|
+
// Sparklines balance useful resolution with compact terminal output.
|
|
5
|
+
const defaultWidth = 20;
|
|
6
|
+
|
|
7
|
+
// Success gives an unconfigured trend a familiar positive semantic tone.
|
|
8
|
+
const defaultTone = "success";
|
|
9
|
+
|
|
10
|
+
// Unicode levels show eight graduated values without needing colour.
|
|
11
|
+
const unicodeSymbols = "▁▂▃▄▅▆▇█";
|
|
12
|
+
|
|
13
|
+
// ASCII levels provide the same eight-step visual fallback.
|
|
14
|
+
const asciiSymbols = "._-:=+*#";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Render a compact trend with stable numeric context.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} options
|
|
20
|
+
* Rendering options.
|
|
21
|
+
* @param {boolean} options.colour
|
|
22
|
+
* Whether ANSI colour should be applied.
|
|
23
|
+
* @param {string} options.label
|
|
24
|
+
* Optional trend label.
|
|
25
|
+
* @param {string} options.tone
|
|
26
|
+
* Semantic colour tone for the trend.
|
|
27
|
+
* @param {boolean} options.unicode
|
|
28
|
+
* Whether graduated Unicode characters should be used.
|
|
29
|
+
* @param {number[]} options.values
|
|
30
|
+
* Finite numeric values in time order.
|
|
31
|
+
* @param {number} options.width
|
|
32
|
+
* Maximum number of plotted samples.
|
|
33
|
+
* @returns {string}
|
|
34
|
+
* Rendered sparkline, or an empty string for unusable values.
|
|
35
|
+
*/
|
|
36
|
+
export function sparkline(options = {}) {
|
|
37
|
+
const values = normaliseValues(options.values);
|
|
38
|
+
|
|
39
|
+
if (values.length === 0) {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const range = resolveRange(values);
|
|
44
|
+
const width = normaliseWidth(options.width);
|
|
45
|
+
const sampledValues = sampleValues(values, width, range);
|
|
46
|
+
const symbols = options.unicode === false ? asciiSymbols : unicodeSymbols;
|
|
47
|
+
const trend = sampledValues.map((value) => renderValue(value, range, symbols)).join("");
|
|
48
|
+
const numericText = `latest=${values.at(-1)} min=${range.minimum} max=${range.maximum}`;
|
|
49
|
+
|
|
50
|
+
const label =
|
|
51
|
+
typeof options.label === "string" && options.label !== "" ? `${options.label}: ` : "";
|
|
52
|
+
|
|
53
|
+
const renderedTrend = renderTrend(trend, options);
|
|
54
|
+
|
|
55
|
+
return `${label}${renderedTrend} ${numericText}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Return finite numeric values when the input is usable.
|
|
60
|
+
*
|
|
61
|
+
* @param {*} values
|
|
62
|
+
* Requested trend values.
|
|
63
|
+
* @returns {number[]}
|
|
64
|
+
* Finite values, or an empty array.
|
|
65
|
+
*/
|
|
66
|
+
function normaliseValues(values) {
|
|
67
|
+
if (
|
|
68
|
+
!Array.isArray(values) ||
|
|
69
|
+
values.length === 0 ||
|
|
70
|
+
values.some((value) => !Number.isFinite(value))
|
|
71
|
+
) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return values;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Return a usable positive integer plot width.
|
|
80
|
+
*
|
|
81
|
+
* @param {number} width
|
|
82
|
+
* Requested plot width.
|
|
83
|
+
* @returns {number}
|
|
84
|
+
* Positive integer width.
|
|
85
|
+
*/
|
|
86
|
+
function normaliseWidth(width) {
|
|
87
|
+
if (!Number.isFinite(width)) {
|
|
88
|
+
return defaultWidth;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return Math.max(Math.floor(width), 1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resolve the shared scale and extrema indexes for a trend.
|
|
96
|
+
*
|
|
97
|
+
* @param {number[]} values
|
|
98
|
+
* Finite trend values.
|
|
99
|
+
* @returns {object}
|
|
100
|
+
* Trend range and first extrema indexes.
|
|
101
|
+
*/
|
|
102
|
+
function resolveRange(values) {
|
|
103
|
+
let minimum = values[0];
|
|
104
|
+
let maximum = values[0];
|
|
105
|
+
let minimumIndex = 0;
|
|
106
|
+
let maximumIndex = 0;
|
|
107
|
+
|
|
108
|
+
for (let index = 1; index < values.length; index += 1) {
|
|
109
|
+
if (values[index] < minimum) {
|
|
110
|
+
minimum = values[index];
|
|
111
|
+
minimumIndex = index;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (values[index] > maximum) {
|
|
115
|
+
maximum = values[index];
|
|
116
|
+
maximumIndex = index;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
maximum,
|
|
122
|
+
maximumIndex,
|
|
123
|
+
minimum,
|
|
124
|
+
minimumIndex,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Reduce values to deterministic plotted samples while retaining key points.
|
|
130
|
+
*
|
|
131
|
+
* @param {number[]} values
|
|
132
|
+
* Finite trend values.
|
|
133
|
+
* @param {number} width
|
|
134
|
+
* Maximum number of plotted samples.
|
|
135
|
+
* @param {object} range
|
|
136
|
+
* Shared trend range and extrema indexes.
|
|
137
|
+
* @returns {number[]}
|
|
138
|
+
* Sampled values in their original order.
|
|
139
|
+
*/
|
|
140
|
+
function sampleValues(values, width, range) {
|
|
141
|
+
if (values.length <= width) {
|
|
142
|
+
return values;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const latestIndex = values.length - 1;
|
|
146
|
+
const requiredIndexes = [...new Set([latestIndex, range.minimumIndex, range.maximumIndex])];
|
|
147
|
+
const selectedIndexes = new Set(requiredIndexes.slice(0, width));
|
|
148
|
+
|
|
149
|
+
const evenlySpacedIndexes = Array.from({ length: width }, (_, index) =>
|
|
150
|
+
Math.round((index * (values.length - 1)) / Math.max(width - 1, 1)),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
for (const index of evenlySpacedIndexes) {
|
|
154
|
+
if (selectedIndexes.size >= width) {
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
selectedIndexes.add(index);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (let index = 0; selectedIndexes.size < width; index += 1) {
|
|
162
|
+
selectedIndexes.add(index);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return [...selectedIndexes].sort((left, right) => left - right).map((index) => values[index]);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Map one value onto the active graduated character set.
|
|
170
|
+
*
|
|
171
|
+
* @param {number} value
|
|
172
|
+
* Value to render.
|
|
173
|
+
* @param {object} range
|
|
174
|
+
* Full trend range used for the shared scale.
|
|
175
|
+
* @param {string} symbols
|
|
176
|
+
* Graduated characters.
|
|
177
|
+
* @returns {string}
|
|
178
|
+
* One trend character.
|
|
179
|
+
*/
|
|
180
|
+
function renderValue(value, range, symbols) {
|
|
181
|
+
if (range.minimum === range.maximum) {
|
|
182
|
+
return symbols[Math.floor((symbols.length - 1) / 2)];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const ratio = (value - range.minimum) / (range.maximum - range.minimum);
|
|
186
|
+
const symbolIndex = Math.round(ratio * (symbols.length - 1));
|
|
187
|
+
|
|
188
|
+
return symbols[symbolIndex];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Apply a semantic tone only when colour output is explicitly enabled.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} trend
|
|
195
|
+
* Uncoloured trend text.
|
|
196
|
+
* @param {object} options
|
|
197
|
+
* Rendering options.
|
|
198
|
+
* @returns {string}
|
|
199
|
+
* Trend text with optional ANSI colour.
|
|
200
|
+
*/
|
|
201
|
+
function renderTrend(trend, options) {
|
|
202
|
+
if (options.colour !== true) {
|
|
203
|
+
return trend;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const tone = getToneColour(options.tone, options) === null ? defaultTone : options.tone;
|
|
207
|
+
|
|
208
|
+
return foreground(trend, tone, options);
|
|
209
|
+
}
|
package/src/theme/colours.js
CHANGED
|
@@ -1,16 +1,107 @@
|
|
|
1
|
-
// Semantic colour
|
|
1
|
+
// Semantic colour roles used by renderers and public integrations.
|
|
2
2
|
export const colourTokens = {
|
|
3
|
-
accent: "
|
|
4
|
-
background: "
|
|
5
|
-
border: "
|
|
6
|
-
danger: "
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
3
|
+
accent: "accent",
|
|
4
|
+
background: "background",
|
|
5
|
+
border: "border",
|
|
6
|
+
danger: "danger",
|
|
7
|
+
diffAddedBackground: "diffAddedBackground",
|
|
8
|
+
diffAddedText: "diffAddedText",
|
|
9
|
+
diffRemovedBackground: "diffRemovedBackground",
|
|
10
|
+
diffRemovedText: "diffRemovedText",
|
|
11
|
+
info: "info",
|
|
12
|
+
muted: "muted",
|
|
13
|
+
panelText: "panelText",
|
|
14
|
+
success: "success",
|
|
15
|
+
surface: "surface",
|
|
16
|
+
surfaceRaised: "surfaceRaised",
|
|
17
|
+
text: "text",
|
|
18
|
+
warning: "warning",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Resolved theme names accepted by the public rendering options.
|
|
22
|
+
export const themes = {
|
|
23
|
+
AUTO: "auto",
|
|
24
|
+
DARK: "dark",
|
|
25
|
+
LIGHT: "light",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const defaultBackground = "default-background";
|
|
29
|
+
const defaultForeground = "default-foreground";
|
|
30
|
+
|
|
31
|
+
// Theme palettes keep normal text on the user's terminal foreground.
|
|
32
|
+
const palettes = {
|
|
33
|
+
[themes.AUTO]: {
|
|
34
|
+
accent: defaultForeground,
|
|
35
|
+
background: defaultBackground,
|
|
36
|
+
border: defaultForeground,
|
|
37
|
+
danger: defaultForeground,
|
|
38
|
+
diffAddedBackground: defaultBackground,
|
|
39
|
+
diffAddedText: defaultForeground,
|
|
40
|
+
diffRemovedBackground: defaultBackground,
|
|
41
|
+
diffRemovedText: defaultForeground,
|
|
42
|
+
info: defaultForeground,
|
|
43
|
+
muted: defaultForeground,
|
|
44
|
+
panelText: defaultForeground,
|
|
45
|
+
success: defaultForeground,
|
|
46
|
+
surface: defaultBackground,
|
|
47
|
+
surfaceRaised: defaultBackground,
|
|
48
|
+
text: defaultForeground,
|
|
49
|
+
warning: defaultForeground,
|
|
50
|
+
chipAgent: defaultBackground,
|
|
51
|
+
chipDanger: defaultBackground,
|
|
52
|
+
chipInfo: defaultBackground,
|
|
53
|
+
chipNeutral: defaultBackground,
|
|
54
|
+
chipSuccess: defaultBackground,
|
|
55
|
+
chipWarning: defaultBackground,
|
|
56
|
+
},
|
|
57
|
+
[themes.DARK]: {
|
|
58
|
+
accent: "ansi-256:183",
|
|
59
|
+
background: defaultBackground,
|
|
60
|
+
border: "ansi-256:239",
|
|
61
|
+
danger: "ansi-256:210",
|
|
62
|
+
diffAddedBackground: "ansi-256:22",
|
|
63
|
+
diffAddedText: "ansi-256:114",
|
|
64
|
+
diffRemovedBackground: "ansi-256:52",
|
|
65
|
+
diffRemovedText: "ansi-256:210",
|
|
66
|
+
info: "ansi-256:117",
|
|
67
|
+
muted: "ansi-256:246",
|
|
68
|
+
panelText: "ansi-256:252",
|
|
69
|
+
success: "ansi-256:114",
|
|
70
|
+
surface: "ansi-256:234",
|
|
71
|
+
surfaceRaised: "ansi-256:236",
|
|
72
|
+
text: defaultForeground,
|
|
73
|
+
warning: "ansi-256:215",
|
|
74
|
+
chipAgent: "ansi-256:58",
|
|
75
|
+
chipDanger: "ansi-256:52",
|
|
76
|
+
chipInfo: "ansi-256:17",
|
|
77
|
+
chipNeutral: "ansi-256:236",
|
|
78
|
+
chipSuccess: "ansi-256:22",
|
|
79
|
+
chipWarning: "ansi-256:58",
|
|
80
|
+
},
|
|
81
|
+
[themes.LIGHT]: {
|
|
82
|
+
accent: "ansi-256:25",
|
|
83
|
+
background: defaultBackground,
|
|
84
|
+
border: "ansi-256:250",
|
|
85
|
+
danger: "ansi-256:160",
|
|
86
|
+
diffAddedBackground: "ansi-256:194",
|
|
87
|
+
diffAddedText: "ansi-256:22",
|
|
88
|
+
diffRemovedBackground: "ansi-256:224",
|
|
89
|
+
diffRemovedText: "ansi-256:124",
|
|
90
|
+
info: "ansi-256:25",
|
|
91
|
+
muted: "ansi-256:242",
|
|
92
|
+
panelText: "ansi-256:238",
|
|
93
|
+
success: "ansi-256:28",
|
|
94
|
+
surface: "ansi-256:255",
|
|
95
|
+
surfaceRaised: "ansi-256:254",
|
|
96
|
+
text: defaultForeground,
|
|
97
|
+
warning: "ansi-256:94",
|
|
98
|
+
chipAgent: "ansi-256:229",
|
|
99
|
+
chipDanger: "ansi-256:224",
|
|
100
|
+
chipInfo: "ansi-256:153",
|
|
101
|
+
chipNeutral: "ansi-256:254",
|
|
102
|
+
chipSuccess: "ansi-256:194",
|
|
103
|
+
chipWarning: "ansi-256:229",
|
|
104
|
+
},
|
|
14
105
|
};
|
|
15
106
|
|
|
16
107
|
// Terminal frame colours for gallery-style previews.
|
|
@@ -19,50 +110,63 @@ export const terminalColours = {
|
|
|
19
110
|
border: "border",
|
|
20
111
|
title: "muted",
|
|
21
112
|
trafficLights: {
|
|
22
|
-
close: "
|
|
23
|
-
minimise: "
|
|
24
|
-
zoom: "
|
|
113
|
+
close: "danger",
|
|
114
|
+
minimise: "warning",
|
|
115
|
+
zoom: "success",
|
|
25
116
|
},
|
|
26
117
|
};
|
|
27
118
|
|
|
28
119
|
// Chip and pill colour pairs for speaker labels and compact states.
|
|
29
120
|
export const chipColours = {
|
|
30
121
|
agent: {
|
|
31
|
-
background: "
|
|
122
|
+
background: "chipAgent",
|
|
32
123
|
foreground: "warning",
|
|
33
124
|
},
|
|
34
125
|
danger: {
|
|
35
|
-
background: "
|
|
126
|
+
background: "chipDanger",
|
|
36
127
|
foreground: "danger",
|
|
37
128
|
},
|
|
38
129
|
info: {
|
|
39
|
-
background: "
|
|
130
|
+
background: "chipInfo",
|
|
40
131
|
foreground: "info",
|
|
41
132
|
},
|
|
42
133
|
neutral: {
|
|
43
|
-
background: "
|
|
134
|
+
background: "chipNeutral",
|
|
44
135
|
foreground: "muted",
|
|
45
136
|
},
|
|
46
137
|
success: {
|
|
47
|
-
background: "
|
|
138
|
+
background: "chipSuccess",
|
|
48
139
|
foreground: "success",
|
|
49
140
|
},
|
|
50
141
|
user: {
|
|
51
|
-
background: "
|
|
142
|
+
background: "chipSuccess",
|
|
52
143
|
foreground: "success",
|
|
53
144
|
},
|
|
54
145
|
warning: {
|
|
55
|
-
background: "
|
|
146
|
+
background: "chipWarning",
|
|
56
147
|
foreground: "warning",
|
|
57
148
|
},
|
|
58
149
|
};
|
|
59
150
|
|
|
151
|
+
// Diff row colours pair forced backgrounds with explicit readable text tokens.
|
|
152
|
+
export const diffColours = {
|
|
153
|
+
added: {
|
|
154
|
+
background: "diffAddedBackground",
|
|
155
|
+
foreground: "diffAddedText",
|
|
156
|
+
},
|
|
157
|
+
removed: {
|
|
158
|
+
background: "diffRemovedBackground",
|
|
159
|
+
foreground: "diffRemovedText",
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
60
163
|
// Panel colours for grouped reports and agent responses.
|
|
61
164
|
export const panelColours = {
|
|
62
165
|
background: "surface",
|
|
63
166
|
border: "border",
|
|
64
167
|
danger: "danger",
|
|
65
168
|
info: "info",
|
|
169
|
+
body: "panelText",
|
|
66
170
|
success: "success",
|
|
67
171
|
warning: "warning",
|
|
68
172
|
};
|
|
@@ -77,7 +181,7 @@ export const tableColours = {
|
|
|
77
181
|
|
|
78
182
|
// Chart colours for simple bars and positive/negative trend segments.
|
|
79
183
|
export const chartColours = {
|
|
80
|
-
barNegative: "
|
|
184
|
+
barNegative: "warning",
|
|
81
185
|
barPositive: "success",
|
|
82
186
|
barWarning: "warning",
|
|
83
187
|
label: "muted",
|
|
@@ -101,41 +205,73 @@ export const toneColours = {
|
|
|
101
205
|
};
|
|
102
206
|
|
|
103
207
|
/**
|
|
104
|
-
* Resolve a colour token by name.
|
|
208
|
+
* Resolve a colour token by name for the active theme.
|
|
105
209
|
*
|
|
106
210
|
* @param {string} colourName
|
|
107
211
|
* Colour token name.
|
|
212
|
+
* @param {object} options
|
|
213
|
+
* Rendering options.
|
|
214
|
+
* @param {string} options.theme
|
|
215
|
+
* Resolved terminal theme.
|
|
108
216
|
* @returns {string|null}
|
|
109
|
-
*
|
|
217
|
+
* ANSI-256, terminal-default, or null when unsupported.
|
|
110
218
|
*/
|
|
111
|
-
export function getColourToken(colourName) {
|
|
112
|
-
return
|
|
219
|
+
export function getColourToken(colourName, options = {}) {
|
|
220
|
+
return palettes[resolveTheme(options.theme)][colourName] ?? null;
|
|
113
221
|
}
|
|
114
222
|
|
|
115
223
|
/**
|
|
116
224
|
* Resolve a tone to a colour token value.
|
|
117
225
|
*
|
|
118
226
|
* @param {string} tone
|
|
119
|
-
*
|
|
227
|
+
* Semantic tone name.
|
|
228
|
+
* @param {object} options
|
|
229
|
+
* Rendering options.
|
|
120
230
|
* @returns {string|null}
|
|
121
|
-
*
|
|
231
|
+
* ANSI-256, terminal-default, or null when unsupported.
|
|
122
232
|
*/
|
|
123
|
-
export function getToneColour(tone) {
|
|
124
|
-
return getColourToken(toneColours[tone]);
|
|
233
|
+
export function getToneColour(tone, options = {}) {
|
|
234
|
+
return getColourToken(toneColours[tone], options);
|
|
125
235
|
}
|
|
126
236
|
|
|
127
237
|
/**
|
|
128
|
-
* Resolve a
|
|
238
|
+
* Resolve a colour token or direct hex value.
|
|
129
239
|
*
|
|
130
240
|
* @param {string} colour
|
|
131
|
-
*
|
|
241
|
+
* Colour token name or hex colour.
|
|
242
|
+
* @param {object} options
|
|
243
|
+
* Rendering options.
|
|
132
244
|
* @returns {string|null}
|
|
133
|
-
*
|
|
245
|
+
* Resolved formatter colour value when supported.
|
|
134
246
|
*/
|
|
135
|
-
export function resolveColourValue(colour) {
|
|
247
|
+
export function resolveColourValue(colour, options = {}) {
|
|
136
248
|
if (colour?.startsWith("#")) {
|
|
137
249
|
return colour;
|
|
138
250
|
}
|
|
139
251
|
|
|
140
|
-
return getColourToken(colour);
|
|
252
|
+
return getColourToken(colour, options);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Check whether a value is a recognised theme name.
|
|
257
|
+
*
|
|
258
|
+
* @param {string} theme
|
|
259
|
+
* Candidate theme name.
|
|
260
|
+
* @returns {boolean}
|
|
261
|
+
* Whether the theme can select a palette.
|
|
262
|
+
*/
|
|
263
|
+
export function isTheme(theme) {
|
|
264
|
+
return Object.values(themes).includes(theme);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Resolve an unknown theme to the safe automatic palette.
|
|
269
|
+
*
|
|
270
|
+
* @param {string} theme
|
|
271
|
+
* Candidate theme name.
|
|
272
|
+
* @returns {string}
|
|
273
|
+
* Recognised theme name.
|
|
274
|
+
*/
|
|
275
|
+
function resolveTheme(theme) {
|
|
276
|
+
return isTheme(theme) ? theme : themes.DARK;
|
|
141
277
|
}
|