@optique/core 1.3.0-dev.2540 → 1.3.0-dev.2554

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,6 +1,15 @@
1
1
 
2
2
  //#region src/displaywidth.ts
3
3
  const ansiRegex = /\x1B(?:\[[0-9;:]*[@-~]|\][^\x1B\x07]*(?:\x1B\\|\x07))/g;
4
+ /**
5
+ * Removes ANSI CSI and OSC sequences before measuring visible text.
6
+ * @param text Rendered terminal text.
7
+ * @returns Text without terminal escape sequences.
8
+ * @internal
9
+ */
10
+ function stripAnsi(text) {
11
+ return text.replace(ansiRegex, "");
12
+ }
4
13
  const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
5
14
  /**
6
15
  * Computes the terminal display width of a string, accounting for
@@ -11,7 +20,7 @@ const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
11
20
  * @internal
12
21
  */
13
22
  function getDisplayWidth(text) {
14
- const stripped = text.replace(ansiRegex, "");
23
+ const stripped = stripAnsi(text);
15
24
  let width = 0;
16
25
  for (const { segment } of segmenter.segment(stripped)) width += graphemeWidth(segment);
17
26
  return width;
@@ -41,4 +50,5 @@ function isEastAsianWide(cp) {
41
50
  }
42
51
 
43
52
  //#endregion
44
- exports.getDisplayWidth = getDisplayWidth;
53
+ exports.getDisplayWidth = getDisplayWidth;
54
+ exports.stripAnsi = stripAnsi;
@@ -1,4 +1,11 @@
1
1
  //#region src/displaywidth.d.ts
2
+ /**
3
+ * Removes ANSI CSI and OSC sequences before measuring visible text.
4
+ * @param text Rendered terminal text.
5
+ * @returns Text without terminal escape sequences.
6
+ * @internal
7
+ */
8
+ declare function stripAnsi(text: string): string;
2
9
  /**
3
10
  * Computes the terminal display width of a string, accounting for
4
11
  * East Asian wide characters, combining marks, emoji, and ANSI escapes.
@@ -9,4 +16,4 @@
9
16
  */
10
17
  declare function getDisplayWidth(text: string): number;
11
18
  //#endregion
12
- export { getDisplayWidth };
19
+ export { getDisplayWidth, stripAnsi };
@@ -1,4 +1,11 @@
1
1
  //#region src/displaywidth.d.ts
2
+ /**
3
+ * Removes ANSI CSI and OSC sequences before measuring visible text.
4
+ * @param text Rendered terminal text.
5
+ * @returns Text without terminal escape sequences.
6
+ * @internal
7
+ */
8
+ declare function stripAnsi(text: string): string;
2
9
  /**
3
10
  * Computes the terminal display width of a string, accounting for
4
11
  * East Asian wide characters, combining marks, emoji, and ANSI escapes.
@@ -9,4 +16,4 @@
9
16
  */
10
17
  declare function getDisplayWidth(text: string): number;
11
18
  //#endregion
12
- export { getDisplayWidth };
19
+ export { getDisplayWidth, stripAnsi };
@@ -1,5 +1,14 @@
1
1
  //#region src/displaywidth.ts
2
2
  const ansiRegex = /\x1B(?:\[[0-9;:]*[@-~]|\][^\x1B\x07]*(?:\x1B\\|\x07))/g;
3
+ /**
4
+ * Removes ANSI CSI and OSC sequences before measuring visible text.
5
+ * @param text Rendered terminal text.
6
+ * @returns Text without terminal escape sequences.
7
+ * @internal
8
+ */
9
+ function stripAnsi(text) {
10
+ return text.replace(ansiRegex, "");
11
+ }
3
12
  const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
4
13
  /**
5
14
  * Computes the terminal display width of a string, accounting for
@@ -10,7 +19,7 @@ const segmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
10
19
  * @internal
11
20
  */
12
21
  function getDisplayWidth(text) {
13
- const stripped = text.replace(ansiRegex, "");
22
+ const stripped = stripAnsi(text);
14
23
  let width = 0;
15
24
  for (const { segment } of segmenter.segment(stripped)) width += graphemeWidth(segment);
16
25
  return width;
@@ -40,4 +49,4 @@ function isEastAsianWide(cp) {
40
49
  }
41
50
 
42
51
  //#endregion
43
- export { getDisplayWidth };
52
+ export { getDisplayWidth, stripAnsi };
package/dist/doc.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  const require_message_registry = require('./message-registry.cjs');
2
- const require_displaywidth = require('./displaywidth.cjs');
3
2
  const require_terminal_internal = require('./terminal-internal.cjs');
4
3
  const require_message = require('./message.cjs');
5
4
  const require_message_renderer = require('./message-renderer.cjs');
5
+ const require_text_layout = require('./text-layout.cjs');
6
6
  const require_validate = require('./validate.cjs');
7
7
  const require_usage = require('./usage.cjs');
8
8
 
@@ -251,8 +251,8 @@ function formatDocPage(programName, page, options = {}) {
251
251
  }
252
252
  return rendered;
253
253
  };
254
- const usageLabel = page.usage != null && options.showUsage !== false ? label("Usage:", "usage") + " " : "";
255
- const usageLabelWidth = require_displaywidth.getDisplayWidth(usageLabel.split("\n").at(-1) ?? "");
254
+ const usageLabel = page.usage != null && options.showUsage !== false ? require_text_layout.spaceAfterLabel(label("Usage:", "usage")) : "";
255
+ const usageLabelWidth = require_text_layout.measureText(usageLabel).lastLineWidth;
256
256
  const defaultStyle = require_terminal_internal.styleCode(options.theme?.annotationStyles?.default ?? { dim: true });
257
257
  const choicesStyle = require_terminal_internal.styleCode(options.theme?.annotationStyles?.choices ?? { dim: true });
258
258
  require_validate.validateProgramName(programName);
@@ -276,6 +276,18 @@ function formatDocPage(programName, page, options = {}) {
276
276
  }
277
277
  const hasContent = (msg) => Array.isArray(msg) && msg.length > 0;
278
278
  const needsDescriptionColumn = (entry) => hasContent(entry.description) || (options.showDefault === true || typeof options.showDefault === "object") && hasContent(entry.default) || (options.showChoices === true || typeof options.showChoices === "object") && hasContent(entry.choices);
279
+ const annotations = /* @__PURE__ */ new Map();
280
+ const annotation = (kind) => {
281
+ const cached = annotations.get(kind);
282
+ if (cached != null) return cached;
283
+ const config = kind === "default" ? options.showDefault : options.showChoices;
284
+ const prefix = punctuation(typeof config === "object" ? config.prefix ?? (kind === "default" ? " [" : " (") : kind === "default" ? " [" : " (", kind === "default" ? "defaultPrefix" : "choicesPrefix");
285
+ const suffix = punctuation(typeof config === "object" ? config.suffix ?? (kind === "default" ? "]" : ")") : kind === "default" ? "]" : ")", kind === "default" ? "defaultSuffix" : "choicesSuffix");
286
+ const choicesLabel = kind === "choices" ? label(typeof options.showChoices === "object" ? options.showChoices.label ?? "choices: " : "choices: ", "choices") : "";
287
+ const layout = require_text_layout.measureAnnotation(prefix + choicesLabel, suffix);
288
+ annotations.set(kind, layout);
289
+ return layout;
290
+ };
279
291
  const automaticTermWidth = () => {
280
292
  let widest;
281
293
  for (const section of page.sections) for (const entry of section.entries) {
@@ -286,7 +298,7 @@ function formatDocPage(programName, page, options = {}) {
286
298
  optionsSeparator: ", ",
287
299
  context: "doc"
288
300
  });
289
- const width = Math.max(...rendered.split("\n").map((line) => require_displaywidth.getDisplayWidth(line)));
301
+ const width = require_text_layout.measureText(rendered).maxLineWidth;
290
302
  widest = widest == null ? width : Math.max(widest, width);
291
303
  }
292
304
  return widest;
@@ -303,16 +315,9 @@ function formatDocPage(programName, page, options = {}) {
303
315
  }
304
316
  if (options.maxWidth != null) {
305
317
  let minDescWidth = 1;
306
- if (needsDescColumn) {
307
- if (options.showDefault && page.sections.some((s) => s.entries.some((e) => hasContent(e.default)))) {
308
- const prefix = punctuation(typeof options.showDefault === "object" ? options.showDefault.prefix ?? " [" : " [", "defaultPrefix");
309
- minDescWidth = Math.max(minDescWidth, require_displaywidth.getDisplayWidth(prefix));
310
- }
311
- if (options.showChoices && page.sections.some((s) => s.entries.some((e) => hasContent(e.choices)))) {
312
- const prefix = punctuation(typeof options.showChoices === "object" ? options.showChoices.prefix ?? " (" : " (", "choicesPrefix");
313
- const choicesLabelText = label(typeof options.showChoices === "object" ? options.showChoices.label ?? "choices: " : "choices: ", "choices");
314
- minDescWidth = Math.max(minDescWidth, require_displaywidth.getDisplayWidth(prefix) + require_displaywidth.getDisplayWidth(choicesLabelText));
315
- }
318
+ if (needsDescColumn) for (const kind of ["default", "choices"]) {
319
+ const enabled = kind === "default" ? options.showDefault : options.showChoices;
320
+ if (enabled && page.sections.some((section) => section.entries.some((entry) => hasContent(entry[kind])))) minDescWidth = Math.max(minDescWidth, annotation(kind).minWidth);
316
321
  }
317
322
  const splitEntryMin = termIndent + 2 + Math.max(2, 2 * minDescWidth - 1);
318
323
  const fixedEntryMin = termIndent + 2 + termWidth + minDescWidth;
@@ -321,11 +326,17 @@ function formatDocPage(programName, page, options = {}) {
321
326
  type: "programName",
322
327
  programName
323
328
  }, options.theme))].map((token) => token.width)) : 0;
324
- const usageMin = page.usage != null && showUsage ? usageLabelWidth + Math.max(programNameWidth, Math.min(maxVisibleAtomicWidth(page.usage, options.theme), programNameWidth + usageLabelWidth)) : 1;
329
+ const usageMin = page.usage != null && showUsage ? Math.max(require_text_layout.measureText(usageLabel).maxLineWidth, usageLabelWidth + Math.max(programNameWidth, Math.min(maxVisibleAtomicWidth(page.usage, options.theme), programNameWidth + usageLabelWidth))) : 1;
325
330
  let sectionMin = 1;
326
- if (hasContent(page.examples)) sectionMin = Math.max(sectionMin, ...label("Examples:", "examples").split("\n").map((line) => require_displaywidth.getDisplayWidth(line)));
327
- if (hasContent(page.author)) sectionMin = Math.max(sectionMin, ...label("Author:", "author").split("\n").map((line) => require_displaywidth.getDisplayWidth(line)));
328
- if (hasContent(page.bugs)) sectionMin = Math.max(sectionMin, ...label("Bugs:", "bugs").split("\n").map((line) => require_displaywidth.getDisplayWidth(line)));
331
+ for (const kind of [
332
+ "examples",
333
+ "author",
334
+ "bugs"
335
+ ]) {
336
+ if (!hasContent(page[kind])) continue;
337
+ const title = kind === "examples" ? "Examples:" : kind === "author" ? "Author:" : "Bugs:";
338
+ sectionMin = Math.max(sectionMin, 3, require_text_layout.measureText(label(title, kind)).maxLineWidth);
339
+ }
329
340
  const minWidth = Math.max(entryMin, usageMin, sectionMin);
330
341
  if (options.maxWidth < minWidth) throw new RangeError(`maxWidth must be at least ${minWidth}, got ${options.maxWidth}.`);
331
342
  if (needsDescColumn && minDescWidth > 1) {
@@ -383,17 +394,21 @@ function formatDocPage(programName, page, options = {}) {
383
394
  output += sectionLabel;
384
395
  }
385
396
  for (const entry of section.entries) {
386
- const term = require_usage.formatUsageTerm(entry.term, {
397
+ const termOptions = {
387
398
  colors: options.colors,
388
399
  theme: options.theme,
389
400
  optionsSeparator: ", ",
390
- context: "doc",
391
- maxWidth: options.maxWidth == null ? void 0 : options.maxWidth - termIndent
401
+ context: "doc"
402
+ };
403
+ const unwrappedTerm = require_usage.formatUsageTerm(entry.term, termOptions);
404
+ const multilineReplacement = require_text_layout.measureText(unwrappedTerm).lineCount > 1;
405
+ const term = options.maxWidth == null ? unwrappedTerm : require_usage.formatUsageTerm(entry.term, {
406
+ ...termOptions,
407
+ maxWidth: options.maxWidth - termIndent
392
408
  });
393
409
  const descColumnWidth = options.maxWidth == null ? void 0 : options.maxWidth - termIndent - effectiveTermWidth - 2;
394
- const termVisibleWidth = lastLineVisibleLength(term);
410
+ const termVisibleWidth = require_text_layout.measureText(term).lastLineWidth;
395
411
  const extraTermOffset = descColumnWidth != null ? Math.max(0, termVisibleWidth - effectiveTermWidth) : 0;
396
- const currentExtraOffset = () => description.includes("\n") ? 0 : extraTermOffset;
397
412
  const descFormatOptions = {
398
413
  colors: options.colors,
399
414
  quotes: !options.colors,
@@ -401,35 +416,26 @@ function formatDocPage(programName, page, options = {}) {
401
416
  initialWidth: extraTermOffset > 0 ? extraTermOffset : void 0
402
417
  };
403
418
  let description = entry.description == null ? "" : formatMessage(entry.description, descFormatOptions);
404
- if (options.showDefault && hasContent(entry.default)) {
405
- const prefix = punctuation(typeof options.showDefault === "object" ? options.showDefault.prefix ?? " [" : " [", "defaultPrefix");
406
- const suffix = punctuation(typeof options.showDefault === "object" ? options.showDefault.suffix ?? "]" : "]", "defaultSuffix");
407
- const prefixWidth = require_displaywidth.getDisplayWidth(prefix);
408
- const suffixWidth = require_displaywidth.getDisplayWidth(suffix);
409
- let defaultStartWidth;
410
- if (descColumnWidth != null) {
411
- const lastW = lastLineVisibleLength(description);
412
- const effectiveLastW = lastW + currentExtraOffset();
413
- if (effectiveLastW + prefixWidth >= descColumnWidth) {
414
- description += "\n";
415
- defaultStartWidth = prefixWidth;
416
- } else defaultStartWidth = effectiveLastW + prefixWidth;
417
- }
418
- const defaultFormatOptions = {
419
- colors: options.colors ? { resetSuffix: defaultStyle } : false,
420
- quotes: !options.colors,
421
- maxWidth: descColumnWidth == null ? void 0 : descColumnWidth - suffixWidth,
422
- initialWidth: defaultStartWidth
423
- };
424
- const defaultContent = formatMessage(entry.default, defaultFormatOptions, options.theme?.annotationStyles?.default ?? { dim: true });
425
- const defaultText = `${prefix}${defaultContent}${opaqueFormatter && options.colors ? defaultStyle : ""}${suffix}`;
426
- const formattedDefault = options.colors ? `${defaultStyle}${defaultText}${defaultStyle ? "\x1B[0m" : ""}` : defaultText;
427
- description += formattedDefault;
428
- }
419
+ let cursor = require_text_layout.placeText(description, {
420
+ line: "",
421
+ column: extraTermOffset
422
+ }).cursor;
423
+ const appendAnnotation = (content, layout, style, ambient, quotes) => {
424
+ const prefix = require_text_layout.placeText(layout.prefix, cursor, descColumnWidth, 1);
425
+ if (prefix.text !== layout.prefix) description += "\n";
426
+ const rendered = formatMessage(content, {
427
+ colors: options.colors ? { resetSuffix: style } : false,
428
+ quotes,
429
+ maxWidth: descColumnWidth == null ? void 0 : descColumnWidth - layout.suffixMetrics.firstLineWidth,
430
+ initialWidth: descColumnWidth != null || layout.prefixMetrics.lineCount > 1 ? prefix.cursor.column : void 0
431
+ }, ambient);
432
+ const suffix = require_text_layout.placeText(layout.suffix, require_text_layout.placeText(rendered, prefix.cursor).cursor, descColumnWidth);
433
+ const annotationText = `${layout.prefix}${rendered}${opaqueFormatter && options.colors ? style : ""}${suffix.text}`;
434
+ description += options.colors ? `${style}${annotationText}${style ? "\x1B[0m" : ""}` : annotationText;
435
+ cursor = suffix.cursor;
436
+ };
437
+ if (options.showDefault && hasContent(entry.default)) appendAnnotation(entry.default, annotation("default"), defaultStyle, defaultAmbient, !options.colors);
429
438
  if (options.showChoices && hasContent(entry.choices)) {
430
- const prefix = punctuation(typeof options.showChoices === "object" ? options.showChoices.prefix ?? " (" : " (", "choicesPrefix");
431
- const suffix = punctuation(typeof options.showChoices === "object" ? options.showChoices.suffix ?? ")" : ")", "choicesSuffix");
432
- const choicesLabelText = label(typeof options.showChoices === "object" ? options.showChoices.label ?? "choices: " : "choices: ", "choices");
433
439
  const maxItems = typeof options.showChoices === "object" ? options.showChoices.maxItems ?? 8 : 8;
434
440
  const terms = Array.isArray(entry.choices) ? entry.choices : [];
435
441
  let truncatedTerms = terms;
@@ -447,31 +453,10 @@ function formatDocPage(programName, page, options = {}) {
447
453
  }
448
454
  if (truncated) truncatedTerms = [...terms.slice(0, cutIndex), require_message.text(", ...")];
449
455
  }
450
- const choicesPrefixWidth = require_displaywidth.getDisplayWidth(prefix);
451
- const choicesSuffixWidth = require_displaywidth.getDisplayWidth(suffix);
452
- const choicesLabelWidth = require_displaywidth.getDisplayWidth(choicesLabelText);
453
- let choicesStartWidth;
454
- if (descColumnWidth != null) {
455
- const lastW = lastLineVisibleLength(description);
456
- const effectiveLastW = lastW + currentExtraOffset();
457
- const prefixLabelLen = choicesPrefixWidth + choicesLabelWidth;
458
- if (effectiveLastW + prefixLabelLen >= descColumnWidth) {
459
- description += "\n";
460
- choicesStartWidth = prefixLabelLen;
461
- } else choicesStartWidth = effectiveLastW + prefixLabelLen;
462
- }
463
- const choicesFormatOptions = {
464
- colors: options.colors ? { resetSuffix: choicesStyle } : false,
465
- quotes: false,
466
- maxWidth: descColumnWidth == null ? void 0 : descColumnWidth - choicesSuffixWidth,
467
- initialWidth: choicesStartWidth
468
- };
469
- const choicesDisplay = formatMessage(truncatedTerms, choicesFormatOptions, options.theme?.annotationStyles?.choices ?? { dim: true });
470
- const choicesText = `${prefix}${choicesLabelText}${choicesDisplay}${opaqueFormatter && options.colors ? choicesStyle : ""}${suffix}`;
471
- const formattedChoices = options.colors ? `${choicesStyle}${choicesText}${choicesStyle ? "\x1B[0m" : ""}` : choicesText;
472
- description += formattedChoices;
456
+ appendAnnotation(truncatedTerms, annotation("choices"), choicesStyle, choicesAmbient, false);
473
457
  }
474
- output += `${" ".repeat(termIndent)}${ansiAwareRightPad(term, effectiveTermWidth)}${description === "" ? "" : ` ${indentLines(description, termIndent + effectiveTermWidth + 2)}`}\n`;
458
+ const paddedTerm = ansiAwareRightPad(term, effectiveTermWidth);
459
+ output += `${" ".repeat(termIndent)}${multilineReplacement ? indentLines(paddedTerm, termIndent) : paddedTerm}${description === "" ? "" : ` ${indentLines(description, termIndent + effectiveTermWidth + 2)}`}\n`;
475
460
  }
476
461
  }
477
462
  if (hasContent(page.examples)) {
@@ -530,21 +515,16 @@ function indentLines(text$1, indent) {
530
515
  * contribute to the rendered width.
531
516
  */
532
517
  function maxVisibleAtomicWidth(usage, theme) {
533
- return Math.max(0, ...usage.flatMap((term) => require_usage.formatUsageTerm(term, {
518
+ return usage.reduce((widest, term) => Math.max(widest, require_text_layout.measureText(require_usage.formatUsageTerm(term, {
534
519
  theme,
535
520
  maxWidth: 1
536
- }).split("\n").map(require_displaywidth.getDisplayWidth)));
521
+ })).maxLineWidth), 0);
537
522
  }
538
523
  function ansiAwareRightPad(text$1, length, char = " ") {
539
- const visibleWidth = lastLineVisibleLength(text$1);
524
+ const visibleWidth = require_text_layout.measureText(text$1).lastLineWidth;
540
525
  if (visibleWidth >= length) return text$1;
541
526
  return text$1 + char.repeat(length - visibleWidth);
542
527
  }
543
- function lastLineVisibleLength(text$1) {
544
- const lastNewline = text$1.lastIndexOf("\n");
545
- const lastLine = lastNewline === -1 ? text$1 : text$1.slice(lastNewline + 1);
546
- return require_displaywidth.getDisplayWidth(lastLine);
547
- }
548
528
 
549
529
  //#endregion
550
530
  exports.cloneDocEntry = cloneDocEntry;
package/dist/doc.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { messageRenderers } from "./message-registry.js";
2
- import { getDisplayWidth } from "./displaywidth.js";
3
2
  import { cacheTerminalTheme, formatTerminalTerm, fragmentTokens, renderTerminalTerm, styleCode } from "./terminal-internal.js";
4
3
  import { cloneMessage, text } from "./message.js";
5
4
  import { resolveMessageFormatter } from "./message-renderer.js";
5
+ import { measureAnnotation, measureText, placeText, spaceAfterLabel } from "./text-layout.js";
6
6
  import { validateLabel, validateProgramName } from "./validate.js";
7
7
  import { cloneUsageTerm, formatUsage, formatUsageTerm, isDocHidden } from "./usage.js";
8
8
 
@@ -251,8 +251,8 @@ function formatDocPage(programName, page, options = {}) {
251
251
  }
252
252
  return rendered;
253
253
  };
254
- const usageLabel = page.usage != null && options.showUsage !== false ? label("Usage:", "usage") + " " : "";
255
- const usageLabelWidth = getDisplayWidth(usageLabel.split("\n").at(-1) ?? "");
254
+ const usageLabel = page.usage != null && options.showUsage !== false ? spaceAfterLabel(label("Usage:", "usage")) : "";
255
+ const usageLabelWidth = measureText(usageLabel).lastLineWidth;
256
256
  const defaultStyle = styleCode(options.theme?.annotationStyles?.default ?? { dim: true });
257
257
  const choicesStyle = styleCode(options.theme?.annotationStyles?.choices ?? { dim: true });
258
258
  validateProgramName(programName);
@@ -276,6 +276,18 @@ function formatDocPage(programName, page, options = {}) {
276
276
  }
277
277
  const hasContent = (msg) => Array.isArray(msg) && msg.length > 0;
278
278
  const needsDescriptionColumn = (entry) => hasContent(entry.description) || (options.showDefault === true || typeof options.showDefault === "object") && hasContent(entry.default) || (options.showChoices === true || typeof options.showChoices === "object") && hasContent(entry.choices);
279
+ const annotations = /* @__PURE__ */ new Map();
280
+ const annotation = (kind) => {
281
+ const cached = annotations.get(kind);
282
+ if (cached != null) return cached;
283
+ const config = kind === "default" ? options.showDefault : options.showChoices;
284
+ const prefix = punctuation(typeof config === "object" ? config.prefix ?? (kind === "default" ? " [" : " (") : kind === "default" ? " [" : " (", kind === "default" ? "defaultPrefix" : "choicesPrefix");
285
+ const suffix = punctuation(typeof config === "object" ? config.suffix ?? (kind === "default" ? "]" : ")") : kind === "default" ? "]" : ")", kind === "default" ? "defaultSuffix" : "choicesSuffix");
286
+ const choicesLabel = kind === "choices" ? label(typeof options.showChoices === "object" ? options.showChoices.label ?? "choices: " : "choices: ", "choices") : "";
287
+ const layout = measureAnnotation(prefix + choicesLabel, suffix);
288
+ annotations.set(kind, layout);
289
+ return layout;
290
+ };
279
291
  const automaticTermWidth = () => {
280
292
  let widest;
281
293
  for (const section of page.sections) for (const entry of section.entries) {
@@ -286,7 +298,7 @@ function formatDocPage(programName, page, options = {}) {
286
298
  optionsSeparator: ", ",
287
299
  context: "doc"
288
300
  });
289
- const width = Math.max(...rendered.split("\n").map((line) => getDisplayWidth(line)));
301
+ const width = measureText(rendered).maxLineWidth;
290
302
  widest = widest == null ? width : Math.max(widest, width);
291
303
  }
292
304
  return widest;
@@ -303,16 +315,9 @@ function formatDocPage(programName, page, options = {}) {
303
315
  }
304
316
  if (options.maxWidth != null) {
305
317
  let minDescWidth = 1;
306
- if (needsDescColumn) {
307
- if (options.showDefault && page.sections.some((s) => s.entries.some((e) => hasContent(e.default)))) {
308
- const prefix = punctuation(typeof options.showDefault === "object" ? options.showDefault.prefix ?? " [" : " [", "defaultPrefix");
309
- minDescWidth = Math.max(minDescWidth, getDisplayWidth(prefix));
310
- }
311
- if (options.showChoices && page.sections.some((s) => s.entries.some((e) => hasContent(e.choices)))) {
312
- const prefix = punctuation(typeof options.showChoices === "object" ? options.showChoices.prefix ?? " (" : " (", "choicesPrefix");
313
- const choicesLabelText = label(typeof options.showChoices === "object" ? options.showChoices.label ?? "choices: " : "choices: ", "choices");
314
- minDescWidth = Math.max(minDescWidth, getDisplayWidth(prefix) + getDisplayWidth(choicesLabelText));
315
- }
318
+ if (needsDescColumn) for (const kind of ["default", "choices"]) {
319
+ const enabled = kind === "default" ? options.showDefault : options.showChoices;
320
+ if (enabled && page.sections.some((section) => section.entries.some((entry) => hasContent(entry[kind])))) minDescWidth = Math.max(minDescWidth, annotation(kind).minWidth);
316
321
  }
317
322
  const splitEntryMin = termIndent + 2 + Math.max(2, 2 * minDescWidth - 1);
318
323
  const fixedEntryMin = termIndent + 2 + termWidth + minDescWidth;
@@ -321,11 +326,17 @@ function formatDocPage(programName, page, options = {}) {
321
326
  type: "programName",
322
327
  programName
323
328
  }, options.theme))].map((token) => token.width)) : 0;
324
- const usageMin = page.usage != null && showUsage ? usageLabelWidth + Math.max(programNameWidth, Math.min(maxVisibleAtomicWidth(page.usage, options.theme), programNameWidth + usageLabelWidth)) : 1;
329
+ const usageMin = page.usage != null && showUsage ? Math.max(measureText(usageLabel).maxLineWidth, usageLabelWidth + Math.max(programNameWidth, Math.min(maxVisibleAtomicWidth(page.usage, options.theme), programNameWidth + usageLabelWidth))) : 1;
325
330
  let sectionMin = 1;
326
- if (hasContent(page.examples)) sectionMin = Math.max(sectionMin, ...label("Examples:", "examples").split("\n").map((line) => getDisplayWidth(line)));
327
- if (hasContent(page.author)) sectionMin = Math.max(sectionMin, ...label("Author:", "author").split("\n").map((line) => getDisplayWidth(line)));
328
- if (hasContent(page.bugs)) sectionMin = Math.max(sectionMin, ...label("Bugs:", "bugs").split("\n").map((line) => getDisplayWidth(line)));
331
+ for (const kind of [
332
+ "examples",
333
+ "author",
334
+ "bugs"
335
+ ]) {
336
+ if (!hasContent(page[kind])) continue;
337
+ const title = kind === "examples" ? "Examples:" : kind === "author" ? "Author:" : "Bugs:";
338
+ sectionMin = Math.max(sectionMin, 3, measureText(label(title, kind)).maxLineWidth);
339
+ }
329
340
  const minWidth = Math.max(entryMin, usageMin, sectionMin);
330
341
  if (options.maxWidth < minWidth) throw new RangeError(`maxWidth must be at least ${minWidth}, got ${options.maxWidth}.`);
331
342
  if (needsDescColumn && minDescWidth > 1) {
@@ -383,17 +394,21 @@ function formatDocPage(programName, page, options = {}) {
383
394
  output += sectionLabel;
384
395
  }
385
396
  for (const entry of section.entries) {
386
- const term = formatUsageTerm(entry.term, {
397
+ const termOptions = {
387
398
  colors: options.colors,
388
399
  theme: options.theme,
389
400
  optionsSeparator: ", ",
390
- context: "doc",
391
- maxWidth: options.maxWidth == null ? void 0 : options.maxWidth - termIndent
401
+ context: "doc"
402
+ };
403
+ const unwrappedTerm = formatUsageTerm(entry.term, termOptions);
404
+ const multilineReplacement = measureText(unwrappedTerm).lineCount > 1;
405
+ const term = options.maxWidth == null ? unwrappedTerm : formatUsageTerm(entry.term, {
406
+ ...termOptions,
407
+ maxWidth: options.maxWidth - termIndent
392
408
  });
393
409
  const descColumnWidth = options.maxWidth == null ? void 0 : options.maxWidth - termIndent - effectiveTermWidth - 2;
394
- const termVisibleWidth = lastLineVisibleLength(term);
410
+ const termVisibleWidth = measureText(term).lastLineWidth;
395
411
  const extraTermOffset = descColumnWidth != null ? Math.max(0, termVisibleWidth - effectiveTermWidth) : 0;
396
- const currentExtraOffset = () => description.includes("\n") ? 0 : extraTermOffset;
397
412
  const descFormatOptions = {
398
413
  colors: options.colors,
399
414
  quotes: !options.colors,
@@ -401,35 +416,26 @@ function formatDocPage(programName, page, options = {}) {
401
416
  initialWidth: extraTermOffset > 0 ? extraTermOffset : void 0
402
417
  };
403
418
  let description = entry.description == null ? "" : formatMessage(entry.description, descFormatOptions);
404
- if (options.showDefault && hasContent(entry.default)) {
405
- const prefix = punctuation(typeof options.showDefault === "object" ? options.showDefault.prefix ?? " [" : " [", "defaultPrefix");
406
- const suffix = punctuation(typeof options.showDefault === "object" ? options.showDefault.suffix ?? "]" : "]", "defaultSuffix");
407
- const prefixWidth = getDisplayWidth(prefix);
408
- const suffixWidth = getDisplayWidth(suffix);
409
- let defaultStartWidth;
410
- if (descColumnWidth != null) {
411
- const lastW = lastLineVisibleLength(description);
412
- const effectiveLastW = lastW + currentExtraOffset();
413
- if (effectiveLastW + prefixWidth >= descColumnWidth) {
414
- description += "\n";
415
- defaultStartWidth = prefixWidth;
416
- } else defaultStartWidth = effectiveLastW + prefixWidth;
417
- }
418
- const defaultFormatOptions = {
419
- colors: options.colors ? { resetSuffix: defaultStyle } : false,
420
- quotes: !options.colors,
421
- maxWidth: descColumnWidth == null ? void 0 : descColumnWidth - suffixWidth,
422
- initialWidth: defaultStartWidth
423
- };
424
- const defaultContent = formatMessage(entry.default, defaultFormatOptions, options.theme?.annotationStyles?.default ?? { dim: true });
425
- const defaultText = `${prefix}${defaultContent}${opaqueFormatter && options.colors ? defaultStyle : ""}${suffix}`;
426
- const formattedDefault = options.colors ? `${defaultStyle}${defaultText}${defaultStyle ? "\x1B[0m" : ""}` : defaultText;
427
- description += formattedDefault;
428
- }
419
+ let cursor = placeText(description, {
420
+ line: "",
421
+ column: extraTermOffset
422
+ }).cursor;
423
+ const appendAnnotation = (content, layout, style, ambient, quotes) => {
424
+ const prefix = placeText(layout.prefix, cursor, descColumnWidth, 1);
425
+ if (prefix.text !== layout.prefix) description += "\n";
426
+ const rendered = formatMessage(content, {
427
+ colors: options.colors ? { resetSuffix: style } : false,
428
+ quotes,
429
+ maxWidth: descColumnWidth == null ? void 0 : descColumnWidth - layout.suffixMetrics.firstLineWidth,
430
+ initialWidth: descColumnWidth != null || layout.prefixMetrics.lineCount > 1 ? prefix.cursor.column : void 0
431
+ }, ambient);
432
+ const suffix = placeText(layout.suffix, placeText(rendered, prefix.cursor).cursor, descColumnWidth);
433
+ const annotationText = `${layout.prefix}${rendered}${opaqueFormatter && options.colors ? style : ""}${suffix.text}`;
434
+ description += options.colors ? `${style}${annotationText}${style ? "\x1B[0m" : ""}` : annotationText;
435
+ cursor = suffix.cursor;
436
+ };
437
+ if (options.showDefault && hasContent(entry.default)) appendAnnotation(entry.default, annotation("default"), defaultStyle, defaultAmbient, !options.colors);
429
438
  if (options.showChoices && hasContent(entry.choices)) {
430
- const prefix = punctuation(typeof options.showChoices === "object" ? options.showChoices.prefix ?? " (" : " (", "choicesPrefix");
431
- const suffix = punctuation(typeof options.showChoices === "object" ? options.showChoices.suffix ?? ")" : ")", "choicesSuffix");
432
- const choicesLabelText = label(typeof options.showChoices === "object" ? options.showChoices.label ?? "choices: " : "choices: ", "choices");
433
439
  const maxItems = typeof options.showChoices === "object" ? options.showChoices.maxItems ?? 8 : 8;
434
440
  const terms = Array.isArray(entry.choices) ? entry.choices : [];
435
441
  let truncatedTerms = terms;
@@ -447,31 +453,10 @@ function formatDocPage(programName, page, options = {}) {
447
453
  }
448
454
  if (truncated) truncatedTerms = [...terms.slice(0, cutIndex), text(", ...")];
449
455
  }
450
- const choicesPrefixWidth = getDisplayWidth(prefix);
451
- const choicesSuffixWidth = getDisplayWidth(suffix);
452
- const choicesLabelWidth = getDisplayWidth(choicesLabelText);
453
- let choicesStartWidth;
454
- if (descColumnWidth != null) {
455
- const lastW = lastLineVisibleLength(description);
456
- const effectiveLastW = lastW + currentExtraOffset();
457
- const prefixLabelLen = choicesPrefixWidth + choicesLabelWidth;
458
- if (effectiveLastW + prefixLabelLen >= descColumnWidth) {
459
- description += "\n";
460
- choicesStartWidth = prefixLabelLen;
461
- } else choicesStartWidth = effectiveLastW + prefixLabelLen;
462
- }
463
- const choicesFormatOptions = {
464
- colors: options.colors ? { resetSuffix: choicesStyle } : false,
465
- quotes: false,
466
- maxWidth: descColumnWidth == null ? void 0 : descColumnWidth - choicesSuffixWidth,
467
- initialWidth: choicesStartWidth
468
- };
469
- const choicesDisplay = formatMessage(truncatedTerms, choicesFormatOptions, options.theme?.annotationStyles?.choices ?? { dim: true });
470
- const choicesText = `${prefix}${choicesLabelText}${choicesDisplay}${opaqueFormatter && options.colors ? choicesStyle : ""}${suffix}`;
471
- const formattedChoices = options.colors ? `${choicesStyle}${choicesText}${choicesStyle ? "\x1B[0m" : ""}` : choicesText;
472
- description += formattedChoices;
456
+ appendAnnotation(truncatedTerms, annotation("choices"), choicesStyle, choicesAmbient, false);
473
457
  }
474
- output += `${" ".repeat(termIndent)}${ansiAwareRightPad(term, effectiveTermWidth)}${description === "" ? "" : ` ${indentLines(description, termIndent + effectiveTermWidth + 2)}`}\n`;
458
+ const paddedTerm = ansiAwareRightPad(term, effectiveTermWidth);
459
+ output += `${" ".repeat(termIndent)}${multilineReplacement ? indentLines(paddedTerm, termIndent) : paddedTerm}${description === "" ? "" : ` ${indentLines(description, termIndent + effectiveTermWidth + 2)}`}\n`;
475
460
  }
476
461
  }
477
462
  if (hasContent(page.examples)) {
@@ -530,21 +515,16 @@ function indentLines(text$1, indent) {
530
515
  * contribute to the rendered width.
531
516
  */
532
517
  function maxVisibleAtomicWidth(usage, theme) {
533
- return Math.max(0, ...usage.flatMap((term) => formatUsageTerm(term, {
518
+ return usage.reduce((widest, term) => Math.max(widest, measureText(formatUsageTerm(term, {
534
519
  theme,
535
520
  maxWidth: 1
536
- }).split("\n").map(getDisplayWidth)));
521
+ })).maxLineWidth), 0);
537
522
  }
538
523
  function ansiAwareRightPad(text$1, length, char = " ") {
539
- const visibleWidth = lastLineVisibleLength(text$1);
524
+ const visibleWidth = measureText(text$1).lastLineWidth;
540
525
  if (visibleWidth >= length) return text$1;
541
526
  return text$1 + char.repeat(length - visibleWidth);
542
527
  }
543
- function lastLineVisibleLength(text$1) {
544
- const lastNewline = text$1.lastIndexOf("\n");
545
- const lastLine = lastNewline === -1 ? text$1 : text$1.slice(lastNewline + 1);
546
- return getDisplayWidth(lastLine);
547
- }
548
528
 
549
529
  //#endregion
550
530
  export { cloneDocEntry, deduplicateDocEntries, deduplicateDocFragments, formatDocPage, isDocEntryHidden };