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

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/dist/doc.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  const require_message_registry = require('./message-registry.cjs');
2
2
  const require_terminal_internal = require('./terminal-internal.cjs');
3
+ const require_text_layout = require('./text-layout.cjs');
3
4
  const require_message = require('./message.cjs');
4
5
  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
 
package/dist/doc.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { messageRenderers } from "./message-registry.js";
2
2
  import { cacheTerminalTheme, formatTerminalTerm, fragmentTokens, renderTerminalTerm, styleCode } from "./terminal-internal.js";
3
+ import { measureAnnotation, measureText, placeText, spaceAfterLabel } from "./text-layout.js";
3
4
  import { cloneMessage, text } from "./message.js";
4
5
  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
 
package/dist/facade.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  const require_internal_annotations = require('./internal/annotations.cjs');
2
2
  const require_terminal_internal = require('./terminal-internal.cjs');
3
+ const require_text_layout = require('./text-layout.cjs');
3
4
  const require_message = require('./message.cjs');
4
5
  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
  const require_doc = require('./doc.cjs');
@@ -517,27 +517,19 @@ function classifyParseFailure(failure, helpOptionNames, helpCommandNames, versio
517
517
  * @since 0.6.0
518
518
  */
519
519
  function handleCompletion(completionArgs, programName, parser, completionParser, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, termWidth, completionCommandDisplayName, completionOptionDisplayName, isOptionMode, sectionOrder, showUsage, rootOptionSuggestions = [], messageFormatter, theme) {
520
- const formatMessage = require_message_renderer.resolveMessageFormatter({
521
- messageFormatter,
522
- theme
523
- });
524
- const errorLabel = () => require_text_layout.spaceAfterLabel(require_terminal_internal.renderTerminalTerm({
525
- type: "errorLabel",
526
- label: "Error:"
527
- }, theme, colors));
528
520
  const shellName = completionArgs[0] || "";
529
521
  const args = completionArgs.slice(1);
530
522
  const callOnError = (code, error) => onError(code, error);
531
523
  const callOnCompletion = (code) => onCompletion(code);
532
524
  if (!shellName) {
533
525
  const error = require_message.message`Missing shell name for completion.`;
534
- const prefix = errorLabel();
535
- stderr(`${prefix}${formatMessage(error, {
526
+ stderr(require_message_renderer.renderErrorMessage(error, {
527
+ messageFormatter,
528
+ theme,
536
529
  colors,
537
530
  quotes: !colors,
538
- maxWidth,
539
- initialWidth: require_text_layout.measureText(prefix).lastLineWidth
540
- })}\n`);
531
+ maxWidth
532
+ }) + "\n");
541
533
  if (completionParser) {
542
534
  const displayName = isOptionMode ? completionOptionDisplayName ?? "--completion" : completionCommandDisplayName ?? "completion";
543
535
  const doc = require_internal_parser.getDocPage(completionParser, [displayName]);
@@ -565,13 +557,13 @@ function handleCompletion(completionArgs, programName, parser, completionParser,
565
557
  available.push(require_message.value(name));
566
558
  }
567
559
  const error = require_message.message`Unsupported shell ${shellName}. Available shells: ${available}.`;
568
- const prefix = errorLabel();
569
- stderr(`${prefix}${formatMessage(error, {
560
+ stderr(require_message_renderer.renderErrorMessage(error, {
561
+ messageFormatter,
562
+ theme,
570
563
  colors,
571
564
  quotes: !colors,
572
- maxWidth,
573
- initialWidth: require_text_layout.measureText(prefix).lastLineWidth
574
- })}`);
565
+ maxWidth
566
+ }));
575
567
  return require_mode_dispatch.dispatchByMode(parser.mode, () => {
576
568
  const result = callOnError(1, error);
577
569
  if (result instanceof Promise) throw new RunParserError("Synchronous parser returned async result.");
@@ -882,14 +874,6 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
882
874
  const { colors, messageFormatter, theme, maxWidth, termWidth, showDefault, showChoices, sectionOrder, showUsage, usageLine, commandList = "recursive", aboveError = "usage", onError = () => {
883
875
  throw new RunParserError("Failed to parse command line arguments.");
884
876
  }, stderr = console.error, stdout = console.log, brief, description, examples, author, bugs, footer } = options;
885
- const formatMessage = require_message_renderer.resolveMessageFormatter({
886
- messageFormatter,
887
- theme
888
- });
889
- const errorLabel = () => require_text_layout.spaceAfterLabel(require_terminal_internal.renderTerminalTerm({
890
- type: "errorLabel",
891
- label: "Error:"
892
- }, theme, colors));
893
877
  let usagePrefix;
894
878
  const usageLabel = () => usagePrefix ??= require_text_layout.spaceAfterLabel(require_terminal_internal.renderTerminalTerm({
895
879
  type: "label",
@@ -1057,14 +1041,13 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
1057
1041
  maxWidth: maxWidth == null ? void 0 : maxWidth - usageLabelWidth(),
1058
1042
  expandCommands: true
1059
1043
  }), usageLabelWidth())}`);
1060
- const prefix = errorLabel();
1061
- const errorMessage = formatMessage(validationError, {
1062
- initialWidth: require_text_layout.measureText(prefix).lastLineWidth,
1063
- maxWidth,
1044
+ stderr(require_message_renderer.renderErrorMessage(validationError, {
1045
+ messageFormatter,
1046
+ theme,
1064
1047
  colors,
1065
- quotes: !colors
1066
- });
1067
- stderr(`${prefix}${errorMessage}`);
1048
+ quotes: !colors,
1049
+ maxWidth
1050
+ }));
1068
1051
  return onError(1, validationError);
1069
1052
  };
1070
1053
  const displayHelp = (doc) => {
@@ -1177,14 +1160,13 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
1177
1160
  maxWidth: maxWidth == null ? void 0 : maxWidth - usageLabelWidth(),
1178
1161
  expandCommands: true
1179
1162
  }), usageLabelWidth())}`);
1180
- const prefix = errorLabel();
1181
- const errorMessage = formatMessage(classified.error, {
1182
- initialWidth: require_text_layout.measureText(prefix).lastLineWidth,
1183
- maxWidth,
1163
+ stderr(require_message_renderer.renderErrorMessage(classified.error, {
1164
+ messageFormatter,
1165
+ theme,
1184
1166
  colors,
1185
- quotes: !colors
1186
- });
1187
- stderr(`${prefix}${errorMessage}`);
1167
+ quotes: !colors,
1168
+ maxWidth
1169
+ }));
1188
1170
  return onError(1, classified.error);
1189
1171
  };
1190
1172
  if (aboveError === "help") {
package/dist/facade.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { injectAnnotations } from "./internal/annotations.js";
2
2
  import { renderTerminalTerm } from "./terminal-internal.js";
3
- import { commandLine, lineBreak, message, optionName, text, value } from "./message.js";
4
- import { resolveMessageFormatter } from "./message-renderer.js";
5
3
  import { measureText, spaceAfterLabel } from "./text-layout.js";
4
+ import { commandLine, lineBreak, message, optionName, text, value } from "./message.js";
5
+ import { renderErrorMessage } from "./message-renderer.js";
6
6
  import { validateCommandNames, validateContextIds, validateMetaNameCollisions, validateOptionNames, validateProgramName } from "./validate.js";
7
7
  import { cloneUsage, formatUsage, isSuggestionHidden, normalizeUsage } from "./usage.js";
8
8
  import { formatDocPage } from "./doc.js";
@@ -517,27 +517,19 @@ function classifyParseFailure(failure, helpOptionNames, helpCommandNames, versio
517
517
  * @since 0.6.0
518
518
  */
519
519
  function handleCompletion(completionArgs, programName, parser, completionParser, stdout, stderr, onCompletion, onError, availableShells, colors, maxWidth, termWidth, completionCommandDisplayName, completionOptionDisplayName, isOptionMode, sectionOrder, showUsage, rootOptionSuggestions = [], messageFormatter, theme) {
520
- const formatMessage = resolveMessageFormatter({
521
- messageFormatter,
522
- theme
523
- });
524
- const errorLabel = () => spaceAfterLabel(renderTerminalTerm({
525
- type: "errorLabel",
526
- label: "Error:"
527
- }, theme, colors));
528
520
  const shellName = completionArgs[0] || "";
529
521
  const args = completionArgs.slice(1);
530
522
  const callOnError = (code, error) => onError(code, error);
531
523
  const callOnCompletion = (code) => onCompletion(code);
532
524
  if (!shellName) {
533
525
  const error = message`Missing shell name for completion.`;
534
- const prefix = errorLabel();
535
- stderr(`${prefix}${formatMessage(error, {
526
+ stderr(renderErrorMessage(error, {
527
+ messageFormatter,
528
+ theme,
536
529
  colors,
537
530
  quotes: !colors,
538
- maxWidth,
539
- initialWidth: measureText(prefix).lastLineWidth
540
- })}\n`);
531
+ maxWidth
532
+ }) + "\n");
541
533
  if (completionParser) {
542
534
  const displayName = isOptionMode ? completionOptionDisplayName ?? "--completion" : completionCommandDisplayName ?? "completion";
543
535
  const doc = getDocPage(completionParser, [displayName]);
@@ -565,13 +557,13 @@ function handleCompletion(completionArgs, programName, parser, completionParser,
565
557
  available.push(value(name));
566
558
  }
567
559
  const error = message`Unsupported shell ${shellName}. Available shells: ${available}.`;
568
- const prefix = errorLabel();
569
- stderr(`${prefix}${formatMessage(error, {
560
+ stderr(renderErrorMessage(error, {
561
+ messageFormatter,
562
+ theme,
570
563
  colors,
571
564
  quotes: !colors,
572
- maxWidth,
573
- initialWidth: measureText(prefix).lastLineWidth
574
- })}`);
565
+ maxWidth
566
+ }));
575
567
  return dispatchByMode(parser.mode, () => {
576
568
  const result = callOnError(1, error);
577
569
  if (result instanceof Promise) throw new RunParserError("Synchronous parser returned async result.");
@@ -882,14 +874,6 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
882
874
  const { colors, messageFormatter, theme, maxWidth, termWidth, showDefault, showChoices, sectionOrder, showUsage, usageLine, commandList = "recursive", aboveError = "usage", onError = () => {
883
875
  throw new RunParserError("Failed to parse command line arguments.");
884
876
  }, stderr = console.error, stdout = console.log, brief, description, examples, author, bugs, footer } = options;
885
- const formatMessage = resolveMessageFormatter({
886
- messageFormatter,
887
- theme
888
- });
889
- const errorLabel = () => spaceAfterLabel(renderTerminalTerm({
890
- type: "errorLabel",
891
- label: "Error:"
892
- }, theme, colors));
893
877
  let usagePrefix;
894
878
  const usageLabel = () => usagePrefix ??= spaceAfterLabel(renderTerminalTerm({
895
879
  type: "label",
@@ -1057,14 +1041,13 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
1057
1041
  maxWidth: maxWidth == null ? void 0 : maxWidth - usageLabelWidth(),
1058
1042
  expandCommands: true
1059
1043
  }), usageLabelWidth())}`);
1060
- const prefix = errorLabel();
1061
- const errorMessage = formatMessage(validationError, {
1062
- initialWidth: measureText(prefix).lastLineWidth,
1063
- maxWidth,
1044
+ stderr(renderErrorMessage(validationError, {
1045
+ messageFormatter,
1046
+ theme,
1064
1047
  colors,
1065
- quotes: !colors
1066
- });
1067
- stderr(`${prefix}${errorMessage}`);
1048
+ quotes: !colors,
1049
+ maxWidth
1050
+ }));
1068
1051
  return onError(1, validationError);
1069
1052
  };
1070
1053
  const displayHelp = (doc) => {
@@ -1177,14 +1160,13 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
1177
1160
  maxWidth: maxWidth == null ? void 0 : maxWidth - usageLabelWidth(),
1178
1161
  expandCommands: true
1179
1162
  }), usageLabelWidth())}`);
1180
- const prefix = errorLabel();
1181
- const errorMessage = formatMessage(classified.error, {
1182
- initialWidth: measureText(prefix).lastLineWidth,
1183
- maxWidth,
1163
+ stderr(renderErrorMessage(classified.error, {
1164
+ messageFormatter,
1165
+ theme,
1184
1166
  colors,
1185
- quotes: !colors
1186
- });
1187
- stderr(`${prefix}${errorMessage}`);
1167
+ quotes: !colors,
1168
+ maxWidth
1169
+ }));
1188
1170
  return onError(1, classified.error);
1189
1171
  };
1190
1172
  if (aboveError === "help") {
@@ -1,11 +1,12 @@
1
1
  const require_displaywidth = require('../displaywidth.cjs');
2
2
  const require_terminal_internal = require('../terminal-internal.cjs');
3
- const require_message_renderer = require('../message-renderer.cjs');
4
3
  const require_text_layout = require('../text-layout.cjs');
4
+ const require_message_renderer = require('../message-renderer.cjs');
5
5
 
6
6
  exports.getDisplayWidth = require_displaywidth.getDisplayWidth;
7
7
  exports.measureText = require_text_layout.measureText;
8
8
  exports.placeText = require_text_layout.placeText;
9
+ exports.renderErrorMessage = require_message_renderer.renderErrorMessage;
9
10
  exports.renderTerminalTerm = require_terminal_internal.renderTerminalTerm;
10
11
  exports.resolveMessageFormatter = require_message_renderer.resolveMessageFormatter;
11
12
  exports.spaceAfterLabel = require_text_layout.spaceAfterLabel;
@@ -1,5 +1,5 @@
1
1
  import { getDisplayWidth } from "../displaywidth.cjs";
2
- import { resolveMessageFormatter } from "../message-renderer.cjs";
2
+ import { renderErrorMessage, resolveMessageFormatter } from "../message-renderer.cjs";
3
3
  import { renderTerminalTerm } from "../terminal-internal.cjs";
4
4
  import { measureText, placeText, spaceAfterLabel } from "../text-layout.cjs";
5
- export { getDisplayWidth, measureText, placeText, renderTerminalTerm, resolveMessageFormatter, spaceAfterLabel };
5
+ export { getDisplayWidth, measureText, placeText, renderErrorMessage, renderTerminalTerm, resolveMessageFormatter, spaceAfterLabel };
@@ -1,5 +1,5 @@
1
1
  import { getDisplayWidth } from "../displaywidth.js";
2
- import { resolveMessageFormatter } from "../message-renderer.js";
2
+ import { renderErrorMessage, resolveMessageFormatter } from "../message-renderer.js";
3
3
  import { renderTerminalTerm } from "../terminal-internal.js";
4
4
  import { measureText, placeText, spaceAfterLabel } from "../text-layout.js";
5
- export { getDisplayWidth, measureText, placeText, renderTerminalTerm, resolveMessageFormatter, spaceAfterLabel };
5
+ export { getDisplayWidth, measureText, placeText, renderErrorMessage, renderTerminalTerm, resolveMessageFormatter, spaceAfterLabel };
@@ -1,6 +1,6 @@
1
1
  import { getDisplayWidth } from "../displaywidth.js";
2
2
  import { renderTerminalTerm } from "../terminal-internal.js";
3
- import { resolveMessageFormatter } from "../message-renderer.js";
4
3
  import { measureText, placeText, spaceAfterLabel } from "../text-layout.js";
4
+ import { renderErrorMessage, resolveMessageFormatter } from "../message-renderer.js";
5
5
 
6
- export { getDisplayWidth, measureText, placeText, renderTerminalTerm, resolveMessageFormatter, spaceAfterLabel };
6
+ export { getDisplayWidth, measureText, placeText, renderErrorMessage, renderTerminalTerm, resolveMessageFormatter, spaceAfterLabel };
@@ -1,4 +1,6 @@
1
1
  const require_message_registry = require('./message-registry.cjs');
2
+ const require_terminal_internal = require('./terminal-internal.cjs');
3
+ const require_text_layout = require('./text-layout.cjs');
2
4
  const require_message = require('./message.cjs');
3
5
 
4
6
  //#region src/message-renderer.ts
@@ -21,6 +23,38 @@ function resolveMessageFormatter(options) {
21
23
  });
22
24
  };
23
25
  }
26
+ /**
27
+ * Assembles a themed error prefix and a message at one layout boundary.
28
+ * Callers own streams, trailing newlines, and the default quoting policy.
29
+ * Opaque formatter output is appended unchanged, including empty output.
30
+ * @param message The original, unprefixed error message.
31
+ * @param options Theme, formatter, and physical-line layout options.
32
+ * @returns The prefixed message without an added trailing newline.
33
+ * @throws {TypeError} If initialWidth is not a finite integer.
34
+ * @throws {RangeError} If initialWidth is negative or a theme color is invalid.
35
+ * @internal
36
+ */
37
+ function renderErrorMessage(message, options = {}) {
38
+ const occupied = options.initialWidth ?? 0;
39
+ if (!Number.isFinite(occupied) || !Number.isInteger(occupied)) throw new TypeError("Initial width must be a finite integer.");
40
+ if (occupied < 0) throw new RangeError("Initial width must be nonnegative.");
41
+ const colors = options.colors;
42
+ const prefix = require_text_layout.placeText(require_text_layout.spaceAfterLabel(require_terminal_internal.renderTerminalTerm({
43
+ type: "errorLabel",
44
+ label: "Error:"
45
+ }, options.theme, typeof colors === "object" ? true : colors, void 0, void 0, typeof colors === "object" ? colors.resetSuffix : void 0)), {
46
+ line: "",
47
+ column: occupied
48
+ }, options.maxWidth);
49
+ const formatted = resolveMessageFormatter(options)(message, {
50
+ colors,
51
+ quotes: options.quotes,
52
+ maxWidth: options.maxWidth,
53
+ initialWidth: prefix.cursor.column
54
+ });
55
+ return prefix.text + formatted;
56
+ }
24
57
 
25
58
  //#endregion
59
+ exports.renderErrorMessage = renderErrorMessage;
26
60
  exports.resolveMessageFormatter = resolveMessageFormatter;
@@ -15,5 +15,20 @@ declare function resolveMessageFormatter(options: {
15
15
  readonly messageFormatter?: MessageFormatter;
16
16
  readonly theme?: TerminalTheme;
17
17
  }): (message: Message, options?: MessageFormatOptions, ambient?: TerminalStyle) => string;
18
+ /**
19
+ * Assembles a themed error prefix and a message at one layout boundary.
20
+ * Callers own streams, trailing newlines, and the default quoting policy.
21
+ * Opaque formatter output is appended unchanged, including empty output.
22
+ * @param message The original, unprefixed error message.
23
+ * @param options Theme, formatter, and physical-line layout options.
24
+ * @returns The prefixed message without an added trailing newline.
25
+ * @throws {TypeError} If initialWidth is not a finite integer.
26
+ * @throws {RangeError} If initialWidth is negative or a theme color is invalid.
27
+ * @internal
28
+ */
29
+ declare function renderErrorMessage(message: Message, options?: MessageFormatOptions & {
30
+ readonly theme?: TerminalTheme;
31
+ readonly messageFormatter?: MessageFormatter;
32
+ }): string;
18
33
  //#endregion
19
- export { resolveMessageFormatter };
34
+ export { renderErrorMessage, resolveMessageFormatter };
@@ -15,5 +15,20 @@ declare function resolveMessageFormatter(options: {
15
15
  readonly messageFormatter?: MessageFormatter;
16
16
  readonly theme?: TerminalTheme;
17
17
  }): (message: Message, options?: MessageFormatOptions, ambient?: TerminalStyle) => string;
18
+ /**
19
+ * Assembles a themed error prefix and a message at one layout boundary.
20
+ * Callers own streams, trailing newlines, and the default quoting policy.
21
+ * Opaque formatter output is appended unchanged, including empty output.
22
+ * @param message The original, unprefixed error message.
23
+ * @param options Theme, formatter, and physical-line layout options.
24
+ * @returns The prefixed message without an added trailing newline.
25
+ * @throws {TypeError} If initialWidth is not a finite integer.
26
+ * @throws {RangeError} If initialWidth is negative or a theme color is invalid.
27
+ * @internal
28
+ */
29
+ declare function renderErrorMessage(message: Message, options?: MessageFormatOptions & {
30
+ readonly theme?: TerminalTheme;
31
+ readonly messageFormatter?: MessageFormatter;
32
+ }): string;
18
33
  //#endregion
19
- export { resolveMessageFormatter };
34
+ export { renderErrorMessage, resolveMessageFormatter };
@@ -1,4 +1,6 @@
1
1
  import { messageRenderers } from "./message-registry.js";
2
+ import { renderTerminalTerm } from "./terminal-internal.js";
3
+ import { placeText, spaceAfterLabel } from "./text-layout.js";
2
4
  import { createMessageFormatter, formatMessage } from "./message.js";
3
5
 
4
6
  //#region src/message-renderer.ts
@@ -21,6 +23,37 @@ function resolveMessageFormatter(options) {
21
23
  });
22
24
  };
23
25
  }
26
+ /**
27
+ * Assembles a themed error prefix and a message at one layout boundary.
28
+ * Callers own streams, trailing newlines, and the default quoting policy.
29
+ * Opaque formatter output is appended unchanged, including empty output.
30
+ * @param message The original, unprefixed error message.
31
+ * @param options Theme, formatter, and physical-line layout options.
32
+ * @returns The prefixed message without an added trailing newline.
33
+ * @throws {TypeError} If initialWidth is not a finite integer.
34
+ * @throws {RangeError} If initialWidth is negative or a theme color is invalid.
35
+ * @internal
36
+ */
37
+ function renderErrorMessage(message, options = {}) {
38
+ const occupied = options.initialWidth ?? 0;
39
+ if (!Number.isFinite(occupied) || !Number.isInteger(occupied)) throw new TypeError("Initial width must be a finite integer.");
40
+ if (occupied < 0) throw new RangeError("Initial width must be nonnegative.");
41
+ const colors = options.colors;
42
+ const prefix = placeText(spaceAfterLabel(renderTerminalTerm({
43
+ type: "errorLabel",
44
+ label: "Error:"
45
+ }, options.theme, typeof colors === "object" ? true : colors, void 0, void 0, typeof colors === "object" ? colors.resetSuffix : void 0)), {
46
+ line: "",
47
+ column: occupied
48
+ }, options.maxWidth);
49
+ const formatted = resolveMessageFormatter(options)(message, {
50
+ colors,
51
+ quotes: options.quotes,
52
+ maxWidth: options.maxWidth,
53
+ initialWidth: prefix.cursor.column
54
+ });
55
+ return prefix.text + formatted;
56
+ }
24
57
 
25
58
  //#endregion
26
- export { resolveMessageFormatter };
59
+ export { renderErrorMessage, resolveMessageFormatter };
@@ -17,11 +17,29 @@ function valuesFormatter(theme) {
17
17
  children: [require_terminal.defaultTerminalTheme.values(term, context)]
18
18
  });
19
19
  }
20
+ /** Default callback identity survives any number of per-pass caches. */
21
+ const defaultFormatters = new WeakSet(Object.values(require_terminal.defaultTerminalTheme).filter((value) => typeof value === "function"));
20
22
  function formatTerminalTerm(term, theme = {}, quotes = false, usage) {
23
+ return formatTerminalLeaf(term, theme, quotes, usage).fragment;
24
+ }
25
+ /**
26
+ * Renders a semantic leaf and records the selected callback's provenance.
27
+ * @param term The semantic leaf to render.
28
+ * @param theme The optional role overrides.
29
+ * @param quotes Whether values should use their quoted representation.
30
+ * @param usage The leaf's position within a usage expression.
31
+ * @returns The fragment and whether its callback has default origin.
32
+ * @throws {TypeError} If the term is invalid or is a message-level hard break.
33
+ * @internal
34
+ */
35
+ function formatTerminalLeaf(term, theme = {}, quotes = false, usage) {
21
36
  const quote = (s) => quotes ? `\`${s}\`` : s;
22
37
  let text;
23
38
  switch (term.type) {
24
- case "text": return terminalText(term.text);
39
+ case "text": return {
40
+ fragment: terminalText(term.text),
41
+ defaultOrigin: true
42
+ };
25
43
  case "lineBreak": throw new TypeError("Hard breaks must be rendered at the message level.");
26
44
  case "optionName":
27
45
  text = quote(term.optionName);
@@ -65,19 +83,25 @@ function formatTerminalTerm(term, theme = {}, quotes = false, usage) {
65
83
  usage,
66
84
  format: (child) => formatTerminalTerm(child, theme, quotes, usage)
67
85
  };
86
+ function render(formatter, value) {
87
+ return {
88
+ fragment: formatter(value, ctx),
89
+ defaultOrigin: defaultFormatters.has(formatter)
90
+ };
91
+ }
68
92
  switch (term.type) {
69
- case "optionName": return (theme.optionName ?? require_terminal.defaultTerminalTheme.optionName)(term, ctx);
70
- case "optionNames": return (theme.optionNames ?? require_terminal.defaultTerminalTheme.optionNames)(term, ctx);
71
- case "metavar": return (theme.metavar ?? require_terminal.defaultTerminalTheme.metavar)(term, ctx);
72
- case "value": return (theme.value ?? require_terminal.defaultTerminalTheme.value)(term, ctx);
73
- case "values": return valuesFormatter(theme)(term, ctx);
74
- case "envVar": return (theme.envVar ?? require_terminal.defaultTerminalTheme.envVar)(term, ctx);
75
- case "commandLine": return (theme.commandLine ?? require_terminal.defaultTerminalTheme.commandLine)(term, ctx);
76
- case "url": return (theme.url ?? require_terminal.defaultTerminalTheme.url)(term, ctx);
77
- case "programName": return (theme.programName ?? require_terminal.defaultTerminalTheme.programName)(term, ctx);
78
- case "label": return (theme.label ?? require_terminal.defaultTerminalTheme.label)(term, ctx);
79
- case "syntaxPunctuation": return (theme.syntaxPunctuation ?? require_terminal.defaultTerminalTheme.syntaxPunctuation)(term, ctx);
80
- case "errorLabel": return (theme.errorLabel ?? require_terminal.defaultTerminalTheme.errorLabel)(term, ctx);
93
+ case "optionName": return render(theme.optionName ?? require_terminal.defaultTerminalTheme.optionName, term);
94
+ case "optionNames": return render(theme.optionNames ?? require_terminal.defaultTerminalTheme.optionNames, term);
95
+ case "metavar": return render(theme.metavar ?? require_terminal.defaultTerminalTheme.metavar, term);
96
+ case "value": return render(theme.value ?? require_terminal.defaultTerminalTheme.value, term);
97
+ case "values": return render(valuesFormatter(theme), term);
98
+ case "envVar": return render(theme.envVar ?? require_terminal.defaultTerminalTheme.envVar, term);
99
+ case "commandLine": return render(theme.commandLine ?? require_terminal.defaultTerminalTheme.commandLine, term);
100
+ case "url": return render(theme.url ?? require_terminal.defaultTerminalTheme.url, term);
101
+ case "programName": return render(theme.programName ?? require_terminal.defaultTerminalTheme.programName, term);
102
+ case "label": return render(theme.label ?? require_terminal.defaultTerminalTheme.label, term);
103
+ case "syntaxPunctuation": return render(theme.syntaxPunctuation ?? require_terminal.defaultTerminalTheme.syntaxPunctuation, term);
104
+ case "errorLabel": return render(theme.errorLabel ?? require_terminal.defaultTerminalTheme.errorLabel, term);
81
105
  }
82
106
  }
83
107
  const colorNames = [
@@ -225,14 +249,14 @@ function serializeTokens(tokens, colors = false, resetSuffix = "", ambient = {})
225
249
  * @since 1.3.0
226
250
  * @internal
227
251
  */
228
- function renderTerminalTerm(term, theme, colors, usage, ambient) {
229
- return serializeTokens(fragmentTokens(formatTerminalTerm(term, theme, false, usage)), colors, "", ambient);
252
+ function renderTerminalTerm(term, theme, colors, usage, ambient, resetSuffix = "") {
253
+ return serializeTokens(fragmentTokens(formatTerminalTerm(term, theme, false, usage)), colors, resetSuffix, ambient);
230
254
  }
231
255
  /** Memoizes semantic leaves for one layout pass, including measurement. */
232
256
  function cacheTerminalTheme(theme = {}) {
233
257
  function cache(formatter) {
234
258
  const fragments = /* @__PURE__ */ new Map();
235
- return (term, context) => {
259
+ const cached = (term, context) => {
236
260
  const key = JSON.stringify([
237
261
  term,
238
262
  context.quotes,
@@ -245,6 +269,8 @@ function cacheTerminalTheme(theme = {}) {
245
269
  }
246
270
  return fragment;
247
271
  };
272
+ if (defaultFormatters.has(formatter)) defaultFormatters.add(cached);
273
+ return cached;
248
274
  }
249
275
  return {
250
276
  optionName: cache(theme.optionName ?? require_terminal.defaultTerminalTheme.optionName),
@@ -268,6 +294,7 @@ function cacheTerminalTheme(theme = {}) {
268
294
 
269
295
  //#endregion
270
296
  exports.cacheTerminalTheme = cacheTerminalTheme;
297
+ exports.formatTerminalLeaf = formatTerminalLeaf;
271
298
  exports.formatTerminalTerm = formatTerminalTerm;
272
299
  exports.fragmentTokens = fragmentTokens;
273
300
  exports.renderTerminalTerm = renderTerminalTerm;
@@ -12,9 +12,27 @@ interface TerminalToken {
12
12
  readonly scopes: readonly Scope[];
13
13
  /** Layout-owned space, removable beside an explicit hard break. */
14
14
  readonly separator?: boolean;
15
+ /** The historical program separator wraps before its following atom. */
16
+ readonly programBoundary?: boolean;
17
+ /** Enables only the legacy default formatter whitespace behavior. */
18
+ readonly legacyWhitespace?: boolean;
15
19
  }
16
20
  declare function terminalText(text: string): TerminalFragment;
17
21
  declare function formatTerminalTerm(term: TerminalTerm, theme?: TerminalTheme, quotes?: boolean, usage?: TerminalFormatContext["usage"]): TerminalFragment;
22
+ /**
23
+ * Renders a semantic leaf and records the selected callback's provenance.
24
+ * @param term The semantic leaf to render.
25
+ * @param theme The optional role overrides.
26
+ * @param quotes Whether values should use their quoted representation.
27
+ * @param usage The leaf's position within a usage expression.
28
+ * @returns The fragment and whether its callback has default origin.
29
+ * @throws {TypeError} If the term is invalid or is a message-level hard break.
30
+ * @internal
31
+ */
32
+ declare function formatTerminalLeaf(term: TerminalTerm, theme?: TerminalTheme, quotes?: boolean, usage?: TerminalFormatContext["usage"]): {
33
+ readonly fragment: TerminalFragment;
34
+ readonly defaultOrigin: boolean;
35
+ };
18
36
  declare function styleCode(style: TerminalStyle): string;
19
37
  declare function fragmentTokens(fragment: TerminalFragment, scopes?: readonly Scope[], inherited?: TerminalStyle): Generator<TerminalToken>;
20
38
  declare function serializeTokens(tokens: Iterable<TerminalToken>, colors?: boolean, resetSuffix?: string, ambient?: TerminalStyle): string;
@@ -31,8 +49,8 @@ declare function serializeTokens(tokens: Iterable<TerminalToken>, colors?: boole
31
49
  * @since 1.3.0
32
50
  * @internal
33
51
  */
34
- declare function renderTerminalTerm(term: TerminalTerm, theme?: TerminalTheme, colors?: boolean, usage?: TerminalFormatContext["usage"], ambient?: TerminalStyle): string;
52
+ declare function renderTerminalTerm(term: TerminalTerm, theme?: TerminalTheme, colors?: boolean, usage?: TerminalFormatContext["usage"], ambient?: TerminalStyle, resetSuffix?: string): string;
35
53
  /** Memoizes semantic leaves for one layout pass, including measurement. */
36
54
  declare function cacheTerminalTheme(theme?: TerminalTheme): TerminalTheme;
37
55
  //#endregion
38
- export { Scope, TerminalToken, cacheTerminalTheme, formatTerminalTerm, fragmentTokens, renderTerminalTerm, serializeTokens, styleCode, terminalText };
56
+ export { Scope, TerminalToken, cacheTerminalTheme, formatTerminalLeaf, formatTerminalTerm, fragmentTokens, renderTerminalTerm, serializeTokens, styleCode, terminalText };
@@ -12,9 +12,27 @@ interface TerminalToken {
12
12
  readonly scopes: readonly Scope[];
13
13
  /** Layout-owned space, removable beside an explicit hard break. */
14
14
  readonly separator?: boolean;
15
+ /** The historical program separator wraps before its following atom. */
16
+ readonly programBoundary?: boolean;
17
+ /** Enables only the legacy default formatter whitespace behavior. */
18
+ readonly legacyWhitespace?: boolean;
15
19
  }
16
20
  declare function terminalText(text: string): TerminalFragment;
17
21
  declare function formatTerminalTerm(term: TerminalTerm, theme?: TerminalTheme, quotes?: boolean, usage?: TerminalFormatContext["usage"]): TerminalFragment;
22
+ /**
23
+ * Renders a semantic leaf and records the selected callback's provenance.
24
+ * @param term The semantic leaf to render.
25
+ * @param theme The optional role overrides.
26
+ * @param quotes Whether values should use their quoted representation.
27
+ * @param usage The leaf's position within a usage expression.
28
+ * @returns The fragment and whether its callback has default origin.
29
+ * @throws {TypeError} If the term is invalid or is a message-level hard break.
30
+ * @internal
31
+ */
32
+ declare function formatTerminalLeaf(term: TerminalTerm, theme?: TerminalTheme, quotes?: boolean, usage?: TerminalFormatContext["usage"]): {
33
+ readonly fragment: TerminalFragment;
34
+ readonly defaultOrigin: boolean;
35
+ };
18
36
  declare function styleCode(style: TerminalStyle): string;
19
37
  declare function fragmentTokens(fragment: TerminalFragment, scopes?: readonly Scope[], inherited?: TerminalStyle): Generator<TerminalToken>;
20
38
  declare function serializeTokens(tokens: Iterable<TerminalToken>, colors?: boolean, resetSuffix?: string, ambient?: TerminalStyle): string;
@@ -31,8 +49,8 @@ declare function serializeTokens(tokens: Iterable<TerminalToken>, colors?: boole
31
49
  * @since 1.3.0
32
50
  * @internal
33
51
  */
34
- declare function renderTerminalTerm(term: TerminalTerm, theme?: TerminalTheme, colors?: boolean, usage?: TerminalFormatContext["usage"], ambient?: TerminalStyle): string;
52
+ declare function renderTerminalTerm(term: TerminalTerm, theme?: TerminalTheme, colors?: boolean, usage?: TerminalFormatContext["usage"], ambient?: TerminalStyle, resetSuffix?: string): string;
35
53
  /** Memoizes semantic leaves for one layout pass, including measurement. */
36
54
  declare function cacheTerminalTheme(theme?: TerminalTheme): TerminalTheme;
37
55
  //#endregion
38
- export { Scope, TerminalToken, cacheTerminalTheme, formatTerminalTerm, fragmentTokens, renderTerminalTerm, serializeTokens, styleCode, terminalText };
56
+ export { Scope, TerminalToken, cacheTerminalTheme, formatTerminalLeaf, formatTerminalTerm, fragmentTokens, renderTerminalTerm, serializeTokens, styleCode, terminalText };
@@ -17,11 +17,29 @@ function valuesFormatter(theme) {
17
17
  children: [defaultTerminalTheme.values(term, context)]
18
18
  });
19
19
  }
20
+ /** Default callback identity survives any number of per-pass caches. */
21
+ const defaultFormatters = new WeakSet(Object.values(defaultTerminalTheme).filter((value) => typeof value === "function"));
20
22
  function formatTerminalTerm(term, theme = {}, quotes = false, usage) {
23
+ return formatTerminalLeaf(term, theme, quotes, usage).fragment;
24
+ }
25
+ /**
26
+ * Renders a semantic leaf and records the selected callback's provenance.
27
+ * @param term The semantic leaf to render.
28
+ * @param theme The optional role overrides.
29
+ * @param quotes Whether values should use their quoted representation.
30
+ * @param usage The leaf's position within a usage expression.
31
+ * @returns The fragment and whether its callback has default origin.
32
+ * @throws {TypeError} If the term is invalid or is a message-level hard break.
33
+ * @internal
34
+ */
35
+ function formatTerminalLeaf(term, theme = {}, quotes = false, usage) {
21
36
  const quote = (s) => quotes ? `\`${s}\`` : s;
22
37
  let text;
23
38
  switch (term.type) {
24
- case "text": return terminalText(term.text);
39
+ case "text": return {
40
+ fragment: terminalText(term.text),
41
+ defaultOrigin: true
42
+ };
25
43
  case "lineBreak": throw new TypeError("Hard breaks must be rendered at the message level.");
26
44
  case "optionName":
27
45
  text = quote(term.optionName);
@@ -65,19 +83,25 @@ function formatTerminalTerm(term, theme = {}, quotes = false, usage) {
65
83
  usage,
66
84
  format: (child) => formatTerminalTerm(child, theme, quotes, usage)
67
85
  };
86
+ function render(formatter, value) {
87
+ return {
88
+ fragment: formatter(value, ctx),
89
+ defaultOrigin: defaultFormatters.has(formatter)
90
+ };
91
+ }
68
92
  switch (term.type) {
69
- case "optionName": return (theme.optionName ?? defaultTerminalTheme.optionName)(term, ctx);
70
- case "optionNames": return (theme.optionNames ?? defaultTerminalTheme.optionNames)(term, ctx);
71
- case "metavar": return (theme.metavar ?? defaultTerminalTheme.metavar)(term, ctx);
72
- case "value": return (theme.value ?? defaultTerminalTheme.value)(term, ctx);
73
- case "values": return valuesFormatter(theme)(term, ctx);
74
- case "envVar": return (theme.envVar ?? defaultTerminalTheme.envVar)(term, ctx);
75
- case "commandLine": return (theme.commandLine ?? defaultTerminalTheme.commandLine)(term, ctx);
76
- case "url": return (theme.url ?? defaultTerminalTheme.url)(term, ctx);
77
- case "programName": return (theme.programName ?? defaultTerminalTheme.programName)(term, ctx);
78
- case "label": return (theme.label ?? defaultTerminalTheme.label)(term, ctx);
79
- case "syntaxPunctuation": return (theme.syntaxPunctuation ?? defaultTerminalTheme.syntaxPunctuation)(term, ctx);
80
- case "errorLabel": return (theme.errorLabel ?? defaultTerminalTheme.errorLabel)(term, ctx);
93
+ case "optionName": return render(theme.optionName ?? defaultTerminalTheme.optionName, term);
94
+ case "optionNames": return render(theme.optionNames ?? defaultTerminalTheme.optionNames, term);
95
+ case "metavar": return render(theme.metavar ?? defaultTerminalTheme.metavar, term);
96
+ case "value": return render(theme.value ?? defaultTerminalTheme.value, term);
97
+ case "values": return render(valuesFormatter(theme), term);
98
+ case "envVar": return render(theme.envVar ?? defaultTerminalTheme.envVar, term);
99
+ case "commandLine": return render(theme.commandLine ?? defaultTerminalTheme.commandLine, term);
100
+ case "url": return render(theme.url ?? defaultTerminalTheme.url, term);
101
+ case "programName": return render(theme.programName ?? defaultTerminalTheme.programName, term);
102
+ case "label": return render(theme.label ?? defaultTerminalTheme.label, term);
103
+ case "syntaxPunctuation": return render(theme.syntaxPunctuation ?? defaultTerminalTheme.syntaxPunctuation, term);
104
+ case "errorLabel": return render(theme.errorLabel ?? defaultTerminalTheme.errorLabel, term);
81
105
  }
82
106
  }
83
107
  const colorNames = [
@@ -225,14 +249,14 @@ function serializeTokens(tokens, colors = false, resetSuffix = "", ambient = {})
225
249
  * @since 1.3.0
226
250
  * @internal
227
251
  */
228
- function renderTerminalTerm(term, theme, colors, usage, ambient) {
229
- return serializeTokens(fragmentTokens(formatTerminalTerm(term, theme, false, usage)), colors, "", ambient);
252
+ function renderTerminalTerm(term, theme, colors, usage, ambient, resetSuffix = "") {
253
+ return serializeTokens(fragmentTokens(formatTerminalTerm(term, theme, false, usage)), colors, resetSuffix, ambient);
230
254
  }
231
255
  /** Memoizes semantic leaves for one layout pass, including measurement. */
232
256
  function cacheTerminalTheme(theme = {}) {
233
257
  function cache(formatter) {
234
258
  const fragments = /* @__PURE__ */ new Map();
235
- return (term, context) => {
259
+ const cached = (term, context) => {
236
260
  const key = JSON.stringify([
237
261
  term,
238
262
  context.quotes,
@@ -245,6 +269,8 @@ function cacheTerminalTheme(theme = {}) {
245
269
  }
246
270
  return fragment;
247
271
  };
272
+ if (defaultFormatters.has(formatter)) defaultFormatters.add(cached);
273
+ return cached;
248
274
  }
249
275
  return {
250
276
  optionName: cache(theme.optionName ?? defaultTerminalTheme.optionName),
@@ -267,4 +293,4 @@ function cacheTerminalTheme(theme = {}) {
267
293
  }
268
294
 
269
295
  //#endregion
270
- export { cacheTerminalTheme, formatTerminalTerm, fragmentTokens, renderTerminalTerm, serializeTokens, styleCode, terminalText };
296
+ export { cacheTerminalTheme, formatTerminalLeaf, formatTerminalTerm, fragmentTokens, renderTerminalTerm, serializeTokens, styleCode, terminalText };
package/dist/usage.cjs CHANGED
@@ -196,34 +196,25 @@ function formatUsage(programName, usage, options = {}) {
196
196
  if (usage.length > 0 && usage.slice(0, -1).every((t) => t.type === "command") && lastTerm.type === "exclusive" && lastTerm.terms.every((t) => t.length > 0 && (t[0].type === "command" || t[0].type === "option" || t[0].type === "argument" || t[0].type === "optional" && t[0].terms.length === 1 && (t[0].terms[0].type === "command" || t[0].terms[0].type === "option" || t[0].terms[0].type === "argument")))) {
197
197
  const lines = [];
198
198
  for (let command of lastTerm.terms) {
199
- const firstTerm$1 = command[0];
200
- if (firstTerm$1?.type === "command" && isUsageHidden(firstTerm$1.hidden)) continue;
199
+ const firstTerm = command[0];
200
+ if (firstTerm?.type === "command" && isUsageHidden(firstTerm.hidden)) continue;
201
201
  if (usage.length > 1) command = [...usage.slice(0, -1), ...command];
202
202
  lines.push(formatUsage(programName, command, options));
203
203
  }
204
204
  if (lines.length > 0) return lines.join("\n");
205
205
  }
206
206
  }
207
- const programTokens = layoutUsageTokens(require_terminal_internal.fragmentTokens(require_terminal_internal.formatTerminalTerm({
208
- type: "programName",
209
- programName
210
- }, options.theme)), options);
211
- const terms = [...normalizeUsageSeparators(formatUsageTerms(usage, options))];
212
- if (terms.length === 0) return require_terminal_internal.serializeTokens(programTokens, options.colors);
213
- const width = programTokens.reduce((n, token) => token.width === -1 ? 0 : n + token.width, 0);
214
- const firstTerm = terms.find((token) => token.text !== "");
215
- const lastProgram = programTokens.findLast((token) => token.text !== "");
216
- if (lastProgram == null || firstTerm == null || lastProgram.width === -1 || firstTerm.width === -1) return require_terminal_internal.serializeTokens(programTokens, options.colors) + wrapUsageTokens(terms, options, width);
217
- const separator = options.maxWidth != null && width + 1 + firstTerm.width > options.maxWidth ? {
218
- text: "\n",
219
- width: -1,
220
- scopes: []
221
- } : {
222
- text: " ",
223
- width: 1,
224
- scopes: []
225
- };
226
- return require_terminal_internal.serializeTokens([...programTokens, separator], options.colors) + wrapUsageTokens(terms, options, separator.width === -1 ? 0 : width + 1);
207
+ return wrapUsageTokens(function* () {
208
+ yield* usageLeafTokens({
209
+ type: "programName",
210
+ programName
211
+ }, options);
212
+ yield {
213
+ ...usageSpace,
214
+ programBoundary: true
215
+ };
216
+ yield* formatUsageTerms(usage, options);
217
+ }(), options);
227
218
  }
228
219
  /**
229
220
  * Normalizes a usage description by flattening nested exclusive terms,
@@ -453,15 +444,23 @@ function filterUsageForDisplay(usage, isHidden = isUsageHidden) {
453
444
  }
454
445
  return terms;
455
446
  }
447
+ const usageSpace = {
448
+ text: " ",
449
+ width: 1,
450
+ scopes: [],
451
+ separator: true
452
+ };
453
+ function* usageLeafTokens(term, options, usage) {
454
+ const { fragment, defaultOrigin } = require_terminal_internal.formatTerminalLeaf(term, options.theme, false, usage);
455
+ for (const token of require_terminal_internal.fragmentTokens(fragment)) yield {
456
+ ...token,
457
+ legacyWhitespace: defaultOrigin
458
+ };
459
+ }
456
460
  function* formatUsageTerms(terms, options) {
457
461
  let i = 0;
458
462
  for (const t of terms) {
459
- if (i > 0) yield {
460
- text: " ",
461
- width: 1,
462
- scopes: [],
463
- separator: true
464
- };
463
+ if (i > 0) yield usageSpace;
465
464
  yield* formatUsageTermInternal(t, options);
466
465
  i++;
467
466
  }
@@ -487,8 +486,8 @@ function formatUsageTerm(term, options = {}) {
487
486
  if (visibleTerms.length < 1) return "";
488
487
  return wrapUsageTokens(formatUsageTermInternal(visibleTerms[0], options), options);
489
488
  }
490
- function wrapUsageTokens(input, options, initialWidth = 0) {
491
- return require_terminal_internal.serializeTokens(layoutUsageTokens(input, options, initialWidth), options.colors);
489
+ function wrapUsageTokens(input, options) {
490
+ return require_terminal_internal.serializeTokens(layoutUsageTokens(input, options), options.colors);
492
491
  }
493
492
  /** Keeps one layout-owned space between nonempty outputs on the same line. */
494
493
  function* normalizeUsageSeparators(input) {
@@ -511,18 +510,33 @@ function* normalizeUsageSeparators(input) {
511
510
  }
512
511
  for (const item of pending) if (!item.separator) yield item;
513
512
  }
514
- function layoutUsageTokens(input, options, initialWidth = 0) {
515
- let lineWidth = initialWidth;
513
+ function layoutUsageTokens(input, options) {
514
+ let lineWidth = 0;
516
515
  const output = [];
517
- for (const token of normalizeUsageSeparators(input)) {
516
+ const tokens = [...normalizeUsageSeparators(input)];
517
+ for (const [index, token] of tokens.entries()) {
518
+ if (token.programBoundary && options.maxWidth != null) {
519
+ const next = tokens.find((item, i) => i > index && item.text !== "");
520
+ if (next != null && lineWidth + token.width + next.width > options.maxWidth) {
521
+ output.push({
522
+ text: "\n",
523
+ width: -1,
524
+ scopes: []
525
+ });
526
+ lineWidth = 0;
527
+ continue;
528
+ }
529
+ }
518
530
  if (token.width === -1) {
519
531
  output.push(token);
520
532
  lineWidth = 0;
521
533
  continue;
522
534
  }
523
535
  if (options.maxWidth != null && lineWidth > 0 && token.width > 0 && lineWidth + token.width > options.maxWidth) {
524
- const last = output.at(-1);
525
- if (last?.text.endsWith(" ") && (!options.colors || last.scopes.length === 0)) output[output.length - 1] = {
536
+ let lastIndex = output.length - 1;
537
+ while (lastIndex >= 0 && output[lastIndex].text === "") lastIndex--;
538
+ const last = output[lastIndex];
539
+ if (last?.text.endsWith(" ") && (last.separator || last.legacyWhitespace && (!options.colors || last.scopes.length === 0))) output[lastIndex] = {
526
540
  ...last,
527
541
  text: last.text.slice(0, -1),
528
542
  width: last.width - 1
@@ -533,7 +547,7 @@ function layoutUsageTokens(input, options, initialWidth = 0) {
533
547
  scopes: []
534
548
  });
535
549
  lineWidth = 0;
536
- if (token.text === " ") continue;
550
+ if (token.separator || token.legacyWhitespace && token.text === " ") continue;
537
551
  }
538
552
  output.push(token);
539
553
  lineWidth += token.width;
@@ -542,7 +556,7 @@ function layoutUsageTokens(input, options, initialWidth = 0) {
542
556
  }
543
557
  function* formatUsageTermInternal(term, options) {
544
558
  function leaf(t, usage) {
545
- return require_terminal_internal.fragmentTokens(require_terminal_internal.formatTerminalTerm(t, options.theme, false, usage));
559
+ return usageLeafTokens(t, options, usage);
546
560
  }
547
561
  function punctuation(punctuation$1, kind) {
548
562
  return leaf({
@@ -551,12 +565,7 @@ function* formatUsageTermInternal(term, options) {
551
565
  kind
552
566
  });
553
567
  }
554
- const space = {
555
- text: " ",
556
- width: 1,
557
- scopes: [],
558
- separator: true
559
- };
568
+ const space = usageSpace;
560
569
  if (term.type === "argument") yield* leaf({
561
570
  type: "metavar",
562
571
  metavar: term.metavar
package/dist/usage.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { getDisplayWidth } from "./displaywidth.js";
2
- import { cacheTerminalTheme, formatTerminalTerm, fragmentTokens, serializeTokens } from "./terminal-internal.js";
2
+ import { cacheTerminalTheme, formatTerminalLeaf, fragmentTokens, serializeTokens } from "./terminal-internal.js";
3
3
  import { validateProgramName } from "./validate.js";
4
4
 
5
5
  //#region src/usage.ts
@@ -196,34 +196,25 @@ function formatUsage(programName, usage, options = {}) {
196
196
  if (usage.length > 0 && usage.slice(0, -1).every((t) => t.type === "command") && lastTerm.type === "exclusive" && lastTerm.terms.every((t) => t.length > 0 && (t[0].type === "command" || t[0].type === "option" || t[0].type === "argument" || t[0].type === "optional" && t[0].terms.length === 1 && (t[0].terms[0].type === "command" || t[0].terms[0].type === "option" || t[0].terms[0].type === "argument")))) {
197
197
  const lines = [];
198
198
  for (let command of lastTerm.terms) {
199
- const firstTerm$1 = command[0];
200
- if (firstTerm$1?.type === "command" && isUsageHidden(firstTerm$1.hidden)) continue;
199
+ const firstTerm = command[0];
200
+ if (firstTerm?.type === "command" && isUsageHidden(firstTerm.hidden)) continue;
201
201
  if (usage.length > 1) command = [...usage.slice(0, -1), ...command];
202
202
  lines.push(formatUsage(programName, command, options));
203
203
  }
204
204
  if (lines.length > 0) return lines.join("\n");
205
205
  }
206
206
  }
207
- const programTokens = layoutUsageTokens(fragmentTokens(formatTerminalTerm({
208
- type: "programName",
209
- programName
210
- }, options.theme)), options);
211
- const terms = [...normalizeUsageSeparators(formatUsageTerms(usage, options))];
212
- if (terms.length === 0) return serializeTokens(programTokens, options.colors);
213
- const width = programTokens.reduce((n, token) => token.width === -1 ? 0 : n + token.width, 0);
214
- const firstTerm = terms.find((token) => token.text !== "");
215
- const lastProgram = programTokens.findLast((token) => token.text !== "");
216
- if (lastProgram == null || firstTerm == null || lastProgram.width === -1 || firstTerm.width === -1) return serializeTokens(programTokens, options.colors) + wrapUsageTokens(terms, options, width);
217
- const separator = options.maxWidth != null && width + 1 + firstTerm.width > options.maxWidth ? {
218
- text: "\n",
219
- width: -1,
220
- scopes: []
221
- } : {
222
- text: " ",
223
- width: 1,
224
- scopes: []
225
- };
226
- return serializeTokens([...programTokens, separator], options.colors) + wrapUsageTokens(terms, options, separator.width === -1 ? 0 : width + 1);
207
+ return wrapUsageTokens(function* () {
208
+ yield* usageLeafTokens({
209
+ type: "programName",
210
+ programName
211
+ }, options);
212
+ yield {
213
+ ...usageSpace,
214
+ programBoundary: true
215
+ };
216
+ yield* formatUsageTerms(usage, options);
217
+ }(), options);
227
218
  }
228
219
  /**
229
220
  * Normalizes a usage description by flattening nested exclusive terms,
@@ -453,15 +444,23 @@ function filterUsageForDisplay(usage, isHidden = isUsageHidden) {
453
444
  }
454
445
  return terms;
455
446
  }
447
+ const usageSpace = {
448
+ text: " ",
449
+ width: 1,
450
+ scopes: [],
451
+ separator: true
452
+ };
453
+ function* usageLeafTokens(term, options, usage) {
454
+ const { fragment, defaultOrigin } = formatTerminalLeaf(term, options.theme, false, usage);
455
+ for (const token of fragmentTokens(fragment)) yield {
456
+ ...token,
457
+ legacyWhitespace: defaultOrigin
458
+ };
459
+ }
456
460
  function* formatUsageTerms(terms, options) {
457
461
  let i = 0;
458
462
  for (const t of terms) {
459
- if (i > 0) yield {
460
- text: " ",
461
- width: 1,
462
- scopes: [],
463
- separator: true
464
- };
463
+ if (i > 0) yield usageSpace;
465
464
  yield* formatUsageTermInternal(t, options);
466
465
  i++;
467
466
  }
@@ -487,8 +486,8 @@ function formatUsageTerm(term, options = {}) {
487
486
  if (visibleTerms.length < 1) return "";
488
487
  return wrapUsageTokens(formatUsageTermInternal(visibleTerms[0], options), options);
489
488
  }
490
- function wrapUsageTokens(input, options, initialWidth = 0) {
491
- return serializeTokens(layoutUsageTokens(input, options, initialWidth), options.colors);
489
+ function wrapUsageTokens(input, options) {
490
+ return serializeTokens(layoutUsageTokens(input, options), options.colors);
492
491
  }
493
492
  /** Keeps one layout-owned space between nonempty outputs on the same line. */
494
493
  function* normalizeUsageSeparators(input) {
@@ -511,18 +510,33 @@ function* normalizeUsageSeparators(input) {
511
510
  }
512
511
  for (const item of pending) if (!item.separator) yield item;
513
512
  }
514
- function layoutUsageTokens(input, options, initialWidth = 0) {
515
- let lineWidth = initialWidth;
513
+ function layoutUsageTokens(input, options) {
514
+ let lineWidth = 0;
516
515
  const output = [];
517
- for (const token of normalizeUsageSeparators(input)) {
516
+ const tokens = [...normalizeUsageSeparators(input)];
517
+ for (const [index, token] of tokens.entries()) {
518
+ if (token.programBoundary && options.maxWidth != null) {
519
+ const next = tokens.find((item, i) => i > index && item.text !== "");
520
+ if (next != null && lineWidth + token.width + next.width > options.maxWidth) {
521
+ output.push({
522
+ text: "\n",
523
+ width: -1,
524
+ scopes: []
525
+ });
526
+ lineWidth = 0;
527
+ continue;
528
+ }
529
+ }
518
530
  if (token.width === -1) {
519
531
  output.push(token);
520
532
  lineWidth = 0;
521
533
  continue;
522
534
  }
523
535
  if (options.maxWidth != null && lineWidth > 0 && token.width > 0 && lineWidth + token.width > options.maxWidth) {
524
- const last = output.at(-1);
525
- if (last?.text.endsWith(" ") && (!options.colors || last.scopes.length === 0)) output[output.length - 1] = {
536
+ let lastIndex = output.length - 1;
537
+ while (lastIndex >= 0 && output[lastIndex].text === "") lastIndex--;
538
+ const last = output[lastIndex];
539
+ if (last?.text.endsWith(" ") && (last.separator || last.legacyWhitespace && (!options.colors || last.scopes.length === 0))) output[lastIndex] = {
526
540
  ...last,
527
541
  text: last.text.slice(0, -1),
528
542
  width: last.width - 1
@@ -533,7 +547,7 @@ function layoutUsageTokens(input, options, initialWidth = 0) {
533
547
  scopes: []
534
548
  });
535
549
  lineWidth = 0;
536
- if (token.text === " ") continue;
550
+ if (token.separator || token.legacyWhitespace && token.text === " ") continue;
537
551
  }
538
552
  output.push(token);
539
553
  lineWidth += token.width;
@@ -542,7 +556,7 @@ function layoutUsageTokens(input, options, initialWidth = 0) {
542
556
  }
543
557
  function* formatUsageTermInternal(term, options) {
544
558
  function leaf(t, usage) {
545
- return fragmentTokens(formatTerminalTerm(t, options.theme, false, usage));
559
+ return usageLeafTokens(t, options, usage);
546
560
  }
547
561
  function punctuation(punctuation$1, kind) {
548
562
  return leaf({
@@ -551,12 +565,7 @@ function* formatUsageTermInternal(term, options) {
551
565
  kind
552
566
  });
553
567
  }
554
- const space = {
555
- text: " ",
556
- width: 1,
557
- scopes: [],
558
- separator: true
559
- };
568
+ const space = usageSpace;
560
569
  if (term.type === "argument") yield* leaf({
561
570
  type: "metavar",
562
571
  metavar: term.metavar
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.3.0-dev.2554",
3
+ "version": "1.3.0-dev.2559",
4
4
  "description": "Type-safe combinatorial command-line interface parser",
5
5
  "keywords": [
6
6
  "CLI",
@@ -237,7 +237,7 @@
237
237
  },
238
238
  "sideEffects": false,
239
239
  "devDependencies": {
240
- "@optique/env": "1.3.0-dev.2554+5fdcbbad",
240
+ "@optique/env": "1.3.0-dev.2559+02079a47",
241
241
  "@types/node": "^24.0.0",
242
242
  "fast-check": "^4.7.0",
243
243
  "tsdown": "^0.13.0",