burgee 0.6.0 → 0.6.1

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.
@@ -1,21 +1,17 @@
1
+ import { type WrapOptions } from "linegauge";
1
2
  /**
2
- * cliui 9 — the column layout yargs' usage renders through — with the string-width,
3
- * strip-ansi and wrap-ansi it depends on, ported for `burgee/yargs`. The wrapping
3
+ * cliui 9 — the column layout yargs' usage renders through — with the wrap-ansi it depends
4
+ * on, ported for `burgee/yargs`. Width and escape-stripping come from linegauge. The wrapping
4
5
  * arithmetic is byte-for-byte the upstream's: yargs' usage tests compare whole help
5
6
  * screens.
6
7
  */
7
- export declare function stripAnsi(str: string): string;
8
- export declare function stringWidth(input: string): number;
9
- interface WrapOptions {
10
- hard?: boolean;
11
- wordWrap?: boolean;
12
- trim?: boolean;
13
- }
8
+ export declare const stripAnsi: (str: string) => string;
9
+ export declare const stringWidth: (input: string) => number;
14
10
  export declare function wrapAnsi(string: string, columns: number, options?: WrapOptions): string;
15
11
  export interface Column {
16
12
  text: string;
17
13
  width?: number | undefined;
18
- align?: 'right' | 'left' | 'center';
14
+ align?: "right" | "left" | "center";
19
15
  padding: number[];
20
16
  border?: boolean;
21
17
  }
@@ -1,221 +1,36 @@
1
+ import { strip, width, wrap } from "linegauge";
1
2
  /**
2
- * cliui 9 — the column layout yargs' usage renders through — with the string-width,
3
- * strip-ansi and wrap-ansi it depends on, ported for `burgee/yargs`. The wrapping
3
+ * cliui 9 — the column layout yargs' usage renders through — with the wrap-ansi it depends
4
+ * on, ported for `burgee/yargs`. Width and escape-stripping come from linegauge. The wrapping
4
5
  * arithmetic is byte-for-byte the upstream's: yargs' usage tests compare whole help
5
6
  * screens.
6
7
  */
7
- const ANSI_PATTERN = '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))';
8
- export function stripAnsi(str) {
9
- return typeof str === 'string' ? str.replace(new RegExp(ANSI_PATTERN, 'g'), '') : str;
10
- }
11
- const segmenter = new Intl.Segmenter();
12
- const EMOJI = new RegExp('^\\p{RGI_Emoji}$', 'v');
13
- function isWide(code) {
14
- return ((code >= 0x1100 && code <= 0x115f) ||
15
- code === 0x2329 ||
16
- code === 0x232a ||
17
- (code >= 0x2e80 && code <= 0x303e) ||
18
- (code >= 0x3041 && code <= 0x33ff) ||
19
- (code >= 0x3400 && code <= 0x4dbf) ||
20
- (code >= 0x4e00 && code <= 0x9fff) ||
21
- (code >= 0xa000 && code <= 0xa4cf) ||
22
- (code >= 0xa960 && code <= 0xa97f) ||
23
- (code >= 0xac00 && code <= 0xd7a3) ||
24
- (code >= 0xf900 && code <= 0xfaff) ||
25
- (code >= 0xfe10 && code <= 0xfe19) ||
26
- (code >= 0xfe30 && code <= 0xfe6f) ||
27
- (code >= 0xff00 && code <= 0xff60) ||
28
- (code >= 0xffe0 && code <= 0xffe6) ||
29
- (code >= 0x1f300 && code <= 0x1f64f) ||
30
- (code >= 0x1f900 && code <= 0x1f9ff) ||
31
- (code >= 0x20000 && code <= 0x3fffd));
32
- }
33
- export function stringWidth(input) {
34
- if (typeof input !== 'string' || input.length === 0)
35
- return 0;
36
- const string = stripAnsi(input);
37
- if (string.length === 0)
38
- return 0;
39
- let width = 0;
40
- for (const { segment } of segmenter.segment(string)) {
41
- const code = segment.codePointAt(0);
42
- if (code <= 0x1f || (code >= 0x7f && code <= 0x9f))
43
- continue;
44
- if ((code >= 0x200b && code <= 0x200f) || code === 0xfeff)
45
- continue;
46
- if (code >= 0x300 && code <= 0x36f)
47
- continue;
48
- if (EMOJI.test(segment)) {
49
- width += 2;
50
- continue;
51
- }
52
- width += isWide(code) ? 2 : 1;
53
- }
54
- return width;
55
- }
56
- /** ansi-styles' open→close map, the part wrap-ansi reads. */
57
- function closeCode(code) {
58
- if (code === 1 || code === 2)
59
- return 22;
60
- if (code === 3)
61
- return 23;
62
- if (code === 4)
63
- return 24;
64
- if (code === 7)
65
- return 27;
66
- if (code === 8)
67
- return 28;
68
- if (code === 9)
69
- return 29;
70
- if ((code >= 30 && code <= 37) || (code >= 90 && code <= 97))
71
- return 39;
72
- if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107))
73
- return 49;
74
- return undefined;
75
- }
76
- const ESC = '\u001B';
77
- const ESCAPES = new Set([ESC, '\u009B']);
78
- const END_CODE = 39;
79
- const ANSI_ESCAPE_BELL = '\u0007';
80
- const ANSI_CSI = '[';
81
- const ANSI_OSC = ']';
82
- const ANSI_SGR_TERMINATOR = 'm';
83
- const ANSI_ESCAPE_LINK = `${ANSI_OSC}8;;`;
84
- const wrapAnsiCode = (code) => `${ESC}${ANSI_CSI}${code}${ANSI_SGR_TERMINATOR}`;
85
- const wrapAnsiHyperlink = (uri) => `${ESC}${ANSI_ESCAPE_LINK}${uri}${ANSI_ESCAPE_BELL}`;
86
- const wordLengths = (string) => string.split(' ').map((character) => stringWidth(character));
87
- function wrapWord(rows, word, columns) {
88
- const characters = [...word];
89
- let isInsideEscape = false;
90
- let isInsideLinkEscape = false;
91
- let visible = stringWidth(stripAnsi(rows.at(-1)));
92
- for (const [index, character] of characters.entries()) {
93
- const characterLength = stringWidth(character);
94
- if (visible + characterLength <= columns)
95
- rows[rows.length - 1] += character;
96
- else {
97
- rows.push(character);
98
- visible = 0;
99
- }
100
- if (ESCAPES.has(character)) {
101
- isInsideEscape = true;
102
- isInsideLinkEscape = characters.slice(index + 1).join('').startsWith(ANSI_ESCAPE_LINK);
103
- }
104
- if (isInsideEscape) {
105
- if (isInsideLinkEscape) {
106
- if (character === ANSI_ESCAPE_BELL) {
107
- isInsideEscape = false;
108
- isInsideLinkEscape = false;
109
- }
110
- }
111
- else if (character === ANSI_SGR_TERMINATOR)
112
- isInsideEscape = false;
113
- continue;
114
- }
115
- visible += characterLength;
116
- if (visible === columns && index < characters.length - 1) {
117
- rows.push('');
118
- visible = 0;
119
- }
120
- }
121
- if (!visible && rows.at(-1).length > 0 && rows.length > 1)
122
- rows[rows.length - 2] += rows.pop();
123
- }
124
- function stringVisibleTrimSpacesRight(string) {
125
- const words = string.split(' ');
126
- let last = words.length;
127
- while (last > 0) {
128
- if (stringWidth(words[last - 1]) > 0)
129
- break;
130
- last--;
131
- }
132
- if (last === words.length)
133
- return string;
134
- return words.slice(0, last).join(' ') + words.slice(last).join('');
135
- }
136
- function execWrap(string, columns, options = {}) {
137
- if (options.trim !== false && string.trim() === '')
138
- return '';
139
- let returnValue = '';
140
- let escapeCode;
141
- let escapeUrl;
142
- const lengths = wordLengths(string);
143
- let rows = [''];
144
- for (const [index, word] of string.split(' ').entries()) {
145
- if (options.trim !== false)
146
- rows[rows.length - 1] = rows.at(-1).trimStart();
147
- let rowLength = stringWidth(rows.at(-1));
148
- if (index !== 0) {
149
- if (rowLength >= columns && (options.wordWrap === false || options.trim === false)) {
150
- rows.push('');
151
- rowLength = 0;
152
- }
153
- if (rowLength > 0 || options.trim === false) {
154
- rows[rows.length - 1] += ' ';
155
- rowLength++;
156
- }
157
- }
158
- const length = lengths[index];
159
- if (options.hard && length > columns) {
160
- const remainingColumns = columns - rowLength;
161
- const breaksStartingThisLine = 1 + Math.floor((length - remainingColumns - 1) / columns);
162
- const breaksStartingNextLine = Math.floor((length - 1) / columns);
163
- if (breaksStartingNextLine < breaksStartingThisLine)
164
- rows.push('');
165
- wrapWord(rows, word, columns);
166
- continue;
167
- }
168
- if (rowLength + length > columns && rowLength > 0 && length > 0) {
169
- if (options.wordWrap === false && rowLength < columns) {
170
- wrapWord(rows, word, columns);
171
- continue;
172
- }
173
- rows.push('');
174
- }
175
- if (rowLength + length > columns && options.wordWrap === false) {
176
- wrapWord(rows, word, columns);
177
- continue;
178
- }
179
- rows[rows.length - 1] += word;
180
- }
181
- if (options.trim !== false)
182
- rows = rows.map(stringVisibleTrimSpacesRight);
183
- const pre = [...rows.join('\n')];
184
- for (const [index, character] of pre.entries()) {
185
- returnValue += character;
186
- if (ESCAPES.has(character)) {
187
- const found = new RegExp(`(?:\\${ANSI_CSI}(?<code>\\d+)m|\\${ANSI_ESCAPE_LINK}(?<uri>.*)${ANSI_ESCAPE_BELL})`).exec(pre.slice(index).join(''));
188
- const groups = found?.groups ?? {};
189
- if (groups.code !== undefined) {
190
- const code = Number.parseFloat(groups.code);
191
- escapeCode = code === END_CODE ? undefined : code;
192
- }
193
- else if (groups.uri !== undefined)
194
- escapeUrl = groups.uri.length === 0 ? undefined : groups.uri;
195
- }
196
- const code = closeCode(Number(escapeCode));
197
- if (pre[index + 1] === '\n') {
198
- if (escapeUrl)
199
- returnValue += wrapAnsiHyperlink('');
200
- if (escapeCode && code)
201
- returnValue += wrapAnsiCode(code);
202
- }
203
- else if (character === '\n') {
204
- if (escapeCode && code)
205
- returnValue += wrapAnsiCode(escapeCode);
206
- if (escapeUrl)
207
- returnValue += wrapAnsiHyperlink(escapeUrl);
208
- }
209
- }
210
- return returnValue;
211
- }
8
+ /*
9
+ * `width` and `strip` are linegauge's — measuring text and removing escapes is the layer
10
+ * below this one, and this file had its own copies of both. They were not merely duplicated,
11
+ * they were wrong: the ITU T.416 sub-parameter form `ESC[38:2::255:0:0m`, which chalk emits
12
+ * for truecolor, left `:2::255:0:0m` behind and measured a 13-column string as 25. linegauge
13
+ * fixed that in its own `strip`; burgee inherited nothing because it was not asking.
14
+ *
15
+ * The names stay, because `usage.ts` and `shim.ts` import them and the upstream port reads
16
+ * the way cliui reads.
17
+ */
18
+ export const stripAnsi = (str) => typeof str === "string" ? strip(str) : str;
19
+ export const stringWidth = (input) => typeof input === "string" ? width(input) : 0;
20
+ /*
21
+ * The wrap is linegauge's too. This file carried a wrap-ansi port — ansi-styles' open→close
22
+ * map, an escape-state machine over every character, hyperlink re-opening — to wrap a line
23
+ * without breaking the styling across it. That is the same job `linegauge/wrap` exists for
24
+ * and grades against wrap-ansi, so burgee kept a second implementation of a thing the layer
25
+ * below already owned.
26
+ *
27
+ * Checked before swapping, because yargs' usage tests compare whole help screens: the two
28
+ * agreed on all ten shapes probed — plain prose at several widths, an unbreakable word,
29
+ * SGR-coloured text, single characters, CJK, an embedded newline, leading and trailing
30
+ * spaces, empty, and a string exactly the column width.
31
+ */
212
32
  export function wrapAnsi(string, columns, options) {
213
- return String(string)
214
- .normalize()
215
- .replace(/\r\n/g, '\n')
216
- .split('\n')
217
- .map((line) => execWrap(line, columns, options))
218
- .join('\n');
33
+ return wrap(String(string).normalize().replace(/\r\n/g, "\n"), columns, options);
219
34
  }
220
35
  const align = { right: alignRight, center: alignCenter };
221
36
  const top = 0;
@@ -240,21 +55,24 @@ export class UI {
240
55
  }
241
56
  div(...args) {
242
57
  if (args.length === 0)
243
- this.div('');
244
- if (this.wrap && this.shouldApplyLayoutDSL(...args) && typeof args[0] === 'string')
58
+ this.div("");
59
+ if (this.wrap &&
60
+ this.shouldApplyLayoutDSL(...args) &&
61
+ typeof args[0] === "string")
245
62
  return this.applyLayoutDSL(args[0]);
246
- const cols = args.map((arg) => (typeof arg === 'string' ? this.colFromString(arg) : arg));
63
+ const cols = args.map((arg) => typeof arg === "string" ? this.colFromString(arg) : arg);
247
64
  this.rows.push(cols);
248
65
  return cols;
249
66
  }
250
67
  shouldApplyLayoutDSL(...args) {
251
- return args.length === 1 && typeof args[0] === 'string' && /[\t\n]/.test(args[0]);
68
+ return (args.length === 1 && typeof args[0] === "string" && /[\t\n]/.test(args[0]));
252
69
  }
253
70
  applyLayoutDSL(str) {
254
- const rows = str.split('\n').map((row) => row.split('\t'));
71
+ const rows = str.split("\n").map((row) => row.split("\t"));
255
72
  let leftColumnWidth = 0;
256
73
  rows.forEach((columns) => {
257
- if (columns.length > 1 && stringWidth(columns[0]) > leftColumnWidth) {
74
+ if (columns.length > 1 &&
75
+ stringWidth(columns[0]) > leftColumnWidth) {
258
76
  leftColumnWidth = Math.min(Math.floor(this.width * 0.5), stringWidth(columns[0]));
259
77
  }
260
78
  });
@@ -271,8 +89,20 @@ export class UI {
271
89
  return { text, padding: this.measurePadding(text) };
272
90
  }
273
91
  measurePadding(str) {
92
+ // An unanchored `\s*$` was the upstream spelling, and it is quadratic: the engine
93
+ // retries at every position, so a cell of 50,000 spaces then an `x` costs 1,346 ms
94
+ // here — CodeQL alert 35, "polynomial regular expression used on uncontrolled data",
95
+ // raised once `stripAnsi` became library input. Spaces, not the tabs the alert names:
96
+ // a tab routes into `applyLayoutDSL`, which splits on it long before this runs.
97
+ // `trim{Start,End}` remove exactly the set `\s` matches (WhiteSpace + LineTerminator)
98
+ // and are linear, so this is the same measurement without the backtracking.
274
99
  const noAnsi = stripAnsi(str);
275
- return [0, /\s*$/.exec(noAnsi)[0].length, 0, /^\s*/.exec(noAnsi)[0].length];
100
+ return [
101
+ 0,
102
+ noAnsi.length - noAnsi.trimEnd().length,
103
+ 0,
104
+ noAnsi.length - noAnsi.trimStart().length,
105
+ ];
276
106
  }
277
107
  toString() {
278
108
  const lines = [];
@@ -282,36 +112,36 @@ export class UI {
282
112
  return lines
283
113
  .filter((line) => !line.hidden)
284
114
  .map((line) => line.text)
285
- .join('\n');
115
+ .join("\n");
286
116
  }
287
117
  rowToString(row, lines) {
288
118
  this.rasterize(row).forEach((rrow, r) => {
289
- let str = '';
119
+ let str = "";
290
120
  rrow.forEach((col, c) => {
291
121
  const column = row[c];
292
122
  const { width } = column;
293
123
  const wrapWidth = this.negatePadding(column);
294
124
  let ts = col;
295
125
  if (wrapWidth > stringWidth(col))
296
- ts += ' '.repeat(wrapWidth - stringWidth(col));
297
- if (column.align && column.align !== 'left' && this.wrap) {
126
+ ts += " ".repeat(wrapWidth - stringWidth(col));
127
+ if (column.align && column.align !== "left" && this.wrap) {
298
128
  const fn = align[column.align];
299
129
  ts = fn(ts, wrapWidth);
300
130
  if (stringWidth(ts) < wrapWidth)
301
- ts += ' '.repeat((width || 0) - stringWidth(ts) - 1);
131
+ ts += " ".repeat((width || 0) - stringWidth(ts) - 1);
302
132
  }
303
133
  const padding = column.padding || [0, 0, 0, 0];
304
134
  if (padding[left])
305
- str += ' '.repeat(padding[left]);
306
- str += addBorder(column, ts, '| ');
135
+ str += " ".repeat(padding[left]);
136
+ str += addBorder(column, ts, "| ");
307
137
  str += ts;
308
- str += addBorder(column, ts, ' |');
138
+ str += addBorder(column, ts, " |");
309
139
  if (padding[right])
310
- str += ' '.repeat(padding[right]);
140
+ str += " ".repeat(padding[right]);
311
141
  if (r === 0 && lines.length > 0)
312
142
  str = this.renderInline(str, lines.at(-1));
313
143
  });
314
- lines.push({ text: str.replace(/ +$/, ''), span: row.span });
144
+ lines.push({ text: str.replace(/ +$/, ""), span: row.span });
315
145
  });
316
146
  return lines;
317
147
  }
@@ -329,7 +159,9 @@ export class UI {
329
159
  if (leadingWhitespace < targetTextWidth)
330
160
  return source;
331
161
  previousLine.hidden = true;
332
- return target.trimEnd() + ' '.repeat(leadingWhitespace - targetTextWidth) + source.trimStart();
162
+ return (target.trimEnd() +
163
+ " ".repeat(leadingWhitespace - targetTextWidth) +
164
+ source.trimStart());
333
165
  }
334
166
  rasterize(row) {
335
167
  const rrows = [];
@@ -338,16 +170,18 @@ export class UI {
338
170
  row.forEach((col, c) => {
339
171
  col.width = widths[c];
340
172
  if (this.wrap)
341
- wrapped = wrapAnsi(col.text, this.negatePadding(col), { hard: true }).split('\n');
173
+ wrapped = wrapAnsi(col.text, this.negatePadding(col), {
174
+ hard: true,
175
+ }).split("\n");
342
176
  else
343
- wrapped = col.text.split('\n');
177
+ wrapped = col.text.split("\n");
344
178
  if (col.border) {
345
- wrapped.unshift(`.${'-'.repeat(this.negatePadding(col) + 2)}.`);
346
- wrapped.push(`'${'-'.repeat(this.negatePadding(col) + 2)}'`);
179
+ wrapped.unshift(`.${"-".repeat(this.negatePadding(col) + 2)}.`);
180
+ wrapped.push(`'${"-".repeat(this.negatePadding(col) + 2)}'`);
347
181
  }
348
182
  if (col.padding) {
349
- wrapped.unshift(...new Array(col.padding[top] || 0).fill(''));
350
- wrapped.push(...new Array(col.padding[bottom] || 0).fill(''));
183
+ wrapped.unshift(...new Array(col.padding[top] || 0).fill(""));
184
+ wrapped.push(...new Array(col.padding[bottom] || 0).fill(""));
351
185
  }
352
186
  wrapped.forEach((str, r) => {
353
187
  if (!rrows[r])
@@ -355,7 +189,7 @@ export class UI {
355
189
  const rrow = rrows[r];
356
190
  for (let i = 0; i < c; i++) {
357
191
  if (rrow[i] === undefined)
358
- rrow.push('');
192
+ rrow.push("");
359
193
  }
360
194
  rrow.push(str);
361
195
  });
@@ -384,18 +218,18 @@ export class UI {
384
218
  return undefined;
385
219
  });
386
220
  const unsetWidth = unset ? Math.floor(remainingWidth / unset) : 0;
387
- return widths.map((w, i) => (w === undefined ? Math.max(unsetWidth, minWidth(row[i])) : w));
221
+ return widths.map((w, i) => w === undefined ? Math.max(unsetWidth, minWidth(row[i])) : w);
388
222
  }
389
223
  }
390
224
  function addBorder(col, ts, style) {
391
225
  if (col.border) {
392
226
  if (/[.']-+[.']/.test(ts))
393
- return '';
227
+ return "";
394
228
  if (ts.trim().length !== 0)
395
229
  return style;
396
- return ' ';
230
+ return " ";
397
231
  }
398
- return '';
232
+ return "";
399
233
  }
400
234
  function minWidth(col) {
401
235
  const padding = col.padding || [];
@@ -405,7 +239,7 @@ function minWidth(col) {
405
239
  return min;
406
240
  }
407
241
  function getWindowWidth() {
408
- if (typeof process === 'object' && process.stdout && process.stdout.columns)
242
+ if (typeof process === "object" && process.stdout && process.stdout.columns)
409
243
  return process.stdout.columns;
410
244
  return 80;
411
245
  }
@@ -413,7 +247,7 @@ function alignRight(str, width) {
413
247
  str = str.trim();
414
248
  const strWidth = stringWidth(str);
415
249
  if (strWidth < width)
416
- return ' '.repeat(width - strWidth) + str;
250
+ return " ".repeat(width - strWidth) + str;
417
251
  return str;
418
252
  }
419
253
  function alignCenter(str, width) {
@@ -421,7 +255,7 @@ function alignCenter(str, width) {
421
255
  const strWidth = stringWidth(str);
422
256
  if (strWidth >= width)
423
257
  return str;
424
- return ' '.repeat((width - strWidth) >> 1) + str;
258
+ return " ".repeat((width - strWidth) >> 1) + str;
425
259
  }
426
260
  export function cliui(opts) {
427
261
  return new UI({ width: opts?.width || getWindowWidth(), wrap: opts?.wrap });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "burgee",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "An agent-native CLI framework, drop-in compatible with commander and yargs. One declaration; help, --json, --schema, --mcp and completions all projected from it.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -96,6 +96,7 @@
96
96
  "provenance": true
97
97
  },
98
98
  "dependencies": {
99
+ "linegauge": "^0.2.0",
99
100
  "roundel": "^0.3.0",
100
101
  "seniority": "^0.1.0"
101
102
  },