@optique/core 1.4.0-dev.2683 → 1.4.0-dev.2708

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
@@ -553,8 +553,16 @@ function formatDocPage(programName, page, options = {}) {
553
553
  column: extraTermOffset
554
554
  }).cursor;
555
555
  const appendAnnotation = (content, layout, style, ambient, quotes) => {
556
- const prefix = require_text_layout.placeText(layout.prefix, cursor, descColumnWidth, 1);
557
- if (prefix.text !== layout.prefix) description += "\n";
556
+ let prefixText = layout.prefix;
557
+ let prefix = require_text_layout.placeText(prefixText, cursor, descColumnWidth, 1);
558
+ if (prefix.text !== layout.prefix) {
559
+ description += "\n";
560
+ prefixText = require_text_layout.trimLeadingSpace(layout.prefix);
561
+ prefix = require_text_layout.placeText(prefixText, {
562
+ line: "",
563
+ column: 0
564
+ });
565
+ }
558
566
  const rendered = formatMessage(content, {
559
567
  colors: options.colors ? { resetSuffix: style } : false,
560
568
  quotes,
@@ -562,7 +570,7 @@ function formatDocPage(programName, page, options = {}) {
562
570
  initialWidth: descColumnWidth != null || layout.prefixMetrics.lineCount > 1 ? prefix.cursor.column : void 0
563
571
  }, ambient);
564
572
  const suffix = require_text_layout.placeText(layout.suffix, require_text_layout.placeText(rendered, prefix.cursor).cursor, descColumnWidth);
565
- const annotationText = `${layout.prefix}${rendered}${opaqueFormatter && options.colors ? style : ""}${suffix.text}`;
573
+ const annotationText = `${prefixText}${rendered}${opaqueFormatter && options.colors ? style : ""}${suffix.text}`;
566
574
  description += options.colors ? `${style}${annotationText}${style ? "\x1B[0m" : ""}` : annotationText;
567
575
  cursor = suffix.cursor;
568
576
  };
package/dist/doc.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { cacheTerminalTheme, renderTerminalTerm, styleCode } from "./terminal-internal.js";
2
- import { measureAnnotation, measureText, placeText, spaceAfterLabel } from "./text-layout.js";
2
+ import { measureAnnotation, measureText, placeText, spaceAfterLabel, trimLeadingSpace } from "./text-layout.js";
3
3
  import { validateLabel, validateProgramName } from "./validate.js";
4
4
  import { cloneUsageTerm, formatUsage, formatUsageTerm, isDocHidden } from "./usage.js";
5
5
  import { hasAutomaticWidth, minimumUsageWidth, reportAutomaticWidth } from "./terminal-width.js";
@@ -553,8 +553,16 @@ function formatDocPage(programName, page, options = {}) {
553
553
  column: extraTermOffset
554
554
  }).cursor;
555
555
  const appendAnnotation = (content, layout, style, ambient, quotes) => {
556
- const prefix = placeText(layout.prefix, cursor, descColumnWidth, 1);
557
- if (prefix.text !== layout.prefix) description += "\n";
556
+ let prefixText = layout.prefix;
557
+ let prefix = placeText(prefixText, cursor, descColumnWidth, 1);
558
+ if (prefix.text !== layout.prefix) {
559
+ description += "\n";
560
+ prefixText = trimLeadingSpace(layout.prefix);
561
+ prefix = placeText(prefixText, {
562
+ line: "",
563
+ column: 0
564
+ });
565
+ }
558
566
  const rendered = formatMessage(content, {
559
567
  colors: options.colors ? { resetSuffix: style } : false,
560
568
  quotes,
@@ -562,7 +570,7 @@ function formatDocPage(programName, page, options = {}) {
562
570
  initialWidth: descColumnWidth != null || layout.prefixMetrics.lineCount > 1 ? prefix.cursor.column : void 0
563
571
  }, ambient);
564
572
  const suffix = placeText(layout.suffix, placeText(rendered, prefix.cursor).cursor, descColumnWidth);
565
- const annotationText = `${layout.prefix}${rendered}${opaqueFormatter && options.colors ? style : ""}${suffix.text}`;
573
+ const annotationText = `${prefixText}${rendered}${opaqueFormatter && options.colors ? style : ""}${suffix.text}`;
566
574
  description += options.colors ? `${style}${annotationText}${style ? "\x1B[0m" : ""}` : annotationText;
567
575
  cursor = suffix.cursor;
568
576
  };
package/dist/message.cjs CHANGED
@@ -347,7 +347,8 @@ function renderMessage(msg, options, theme, ambient) {
347
347
  yield {
348
348
  text: match[0],
349
349
  width: require_displaywidth.getDisplayWidth(match[0]),
350
- scopes: []
350
+ scopes: [],
351
+ trimOnWrap: true
351
352
  };
352
353
  }
353
354
  }
@@ -356,7 +357,8 @@ function renderMessage(msg, options, theme, ambient) {
356
357
  if (normalizedText.trim() === "" && normalizedText.length > 0) yield {
357
358
  text: " ",
358
359
  width: 1,
359
- scopes: []
360
+ scopes: [],
361
+ trimOnWrap: true
360
362
  };
361
363
  else {
362
364
  wordPattern.lastIndex = 0;
@@ -366,7 +368,8 @@ function renderMessage(msg, options, theme, ambient) {
366
368
  yield {
367
369
  text: match[0],
368
370
  width: require_displaywidth.getDisplayWidth(match[0]),
369
- scopes: []
371
+ scopes: [],
372
+ trimOnWrap: true
370
373
  };
371
374
  }
372
375
  }
@@ -383,22 +386,46 @@ function renderMessage(msg, options, theme, ambient) {
383
386
  }
384
387
  const tokens = [];
385
388
  let totalWidth = initialWidth;
386
- for (const token of stream()) {
389
+ let pendingWrap = false;
390
+ let atWrappedLineStart = false;
391
+ for (let token of stream()) {
387
392
  if (token.width === -1) {
388
393
  tokens.push(token);
389
394
  totalWidth = 0;
395
+ pendingWrap = false;
396
+ atWrappedLineStart = false;
397
+ continue;
398
+ }
399
+ const wraps = pendingWrap || options.maxWidth != null && totalWidth > 0 && totalWidth + token.width > options.maxWidth;
400
+ if ((wraps || atWrappedLineStart) && token.trimOnWrap) {
401
+ const text$1 = token.text.trimStart();
402
+ token = {
403
+ ...token,
404
+ text: text$1,
405
+ width: token.width - require_displaywidth.getDisplayWidth(token.text.slice(0, token.text.length - text$1.length))
406
+ };
407
+ if (text$1 === "") {
408
+ if (wraps) pendingWrap = true;
409
+ continue;
410
+ }
411
+ }
412
+ if (pendingWrap && require_displaywidth.stripAnsi(token.text) === "") {
413
+ tokens.push(token);
390
414
  continue;
391
415
  }
392
- if (options.maxWidth != null && totalWidth > 0 && totalWidth + token.width > options.maxWidth) {
416
+ if (wraps) {
393
417
  tokens.push({
394
418
  text: "\n",
395
419
  width: -1,
396
420
  scopes: []
397
421
  });
398
422
  totalWidth = 0;
423
+ pendingWrap = false;
424
+ atWrappedLineStart = true;
399
425
  }
400
426
  tokens.push(token);
401
427
  totalWidth += token.width;
428
+ if (token.width > 0) atWrappedLineStart = false;
402
429
  }
403
430
  return require_terminal_internal.serializeTokens(tokens, useColors, ambient == null ? resetSuffix : "", ambient);
404
431
  }
package/dist/message.js CHANGED
@@ -1,4 +1,4 @@
1
- import { getDisplayWidth } from "./displaywidth.js";
1
+ import { getDisplayWidth, stripAnsi } from "./displaywidth.js";
2
2
  import { formatTerminalTerm, fragmentTokens, serializeTokens } from "./terminal-internal.js";
3
3
  import { messageRenderers } from "./message-registry.js";
4
4
 
@@ -347,7 +347,8 @@ function renderMessage(msg, options, theme, ambient) {
347
347
  yield {
348
348
  text: match[0],
349
349
  width: getDisplayWidth(match[0]),
350
- scopes: []
350
+ scopes: [],
351
+ trimOnWrap: true
351
352
  };
352
353
  }
353
354
  }
@@ -356,7 +357,8 @@ function renderMessage(msg, options, theme, ambient) {
356
357
  if (normalizedText.trim() === "" && normalizedText.length > 0) yield {
357
358
  text: " ",
358
359
  width: 1,
359
- scopes: []
360
+ scopes: [],
361
+ trimOnWrap: true
360
362
  };
361
363
  else {
362
364
  wordPattern.lastIndex = 0;
@@ -366,7 +368,8 @@ function renderMessage(msg, options, theme, ambient) {
366
368
  yield {
367
369
  text: match[0],
368
370
  width: getDisplayWidth(match[0]),
369
- scopes: []
371
+ scopes: [],
372
+ trimOnWrap: true
370
373
  };
371
374
  }
372
375
  }
@@ -383,22 +386,46 @@ function renderMessage(msg, options, theme, ambient) {
383
386
  }
384
387
  const tokens = [];
385
388
  let totalWidth = initialWidth;
386
- for (const token of stream()) {
389
+ let pendingWrap = false;
390
+ let atWrappedLineStart = false;
391
+ for (let token of stream()) {
387
392
  if (token.width === -1) {
388
393
  tokens.push(token);
389
394
  totalWidth = 0;
395
+ pendingWrap = false;
396
+ atWrappedLineStart = false;
397
+ continue;
398
+ }
399
+ const wraps = pendingWrap || options.maxWidth != null && totalWidth > 0 && totalWidth + token.width > options.maxWidth;
400
+ if ((wraps || atWrappedLineStart) && token.trimOnWrap) {
401
+ const text$1 = token.text.trimStart();
402
+ token = {
403
+ ...token,
404
+ text: text$1,
405
+ width: token.width - getDisplayWidth(token.text.slice(0, token.text.length - text$1.length))
406
+ };
407
+ if (text$1 === "") {
408
+ if (wraps) pendingWrap = true;
409
+ continue;
410
+ }
411
+ }
412
+ if (pendingWrap && stripAnsi(token.text) === "") {
413
+ tokens.push(token);
390
414
  continue;
391
415
  }
392
- if (options.maxWidth != null && totalWidth > 0 && totalWidth + token.width > options.maxWidth) {
416
+ if (wraps) {
393
417
  tokens.push({
394
418
  text: "\n",
395
419
  width: -1,
396
420
  scopes: []
397
421
  });
398
422
  totalWidth = 0;
423
+ pendingWrap = false;
424
+ atWrappedLineStart = true;
399
425
  }
400
426
  tokens.push(token);
401
427
  totalWidth += token.width;
428
+ if (token.width > 0) atWrappedLineStart = false;
402
429
  }
403
430
  return serializeTokens(tokens, useColors, ambient == null ? resetSuffix : "", ambient);
404
431
  }
@@ -1,4 +1,5 @@
1
1
  const require_displaywidth = require('./displaywidth.cjs');
2
+ const require_terminal_separators = require('./terminal-separators.cjs');
2
3
  const require_terminal = require('./terminal.cjs');
3
4
 
4
5
  //#region src/terminal-internal.ts
@@ -151,6 +152,7 @@ function sameStyle(a, b) {
151
152
  }
152
153
  function* fragmentTokens(fragment, scopes = [], inherited = {}) {
153
154
  if (fragment.type === "text") {
155
+ const marks = require_terminal_separators.wrapTrimmableFragments.has(fragment) ? { trimOnWrap: true } : {};
154
156
  const lines = fragment.text.split("\n");
155
157
  for (let i = 0; i < lines.length; i++) {
156
158
  if (i > 0) yield {
@@ -161,7 +163,8 @@ function* fragmentTokens(fragment, scopes = [], inherited = {}) {
161
163
  if (lines[i].length > 0 || lines.length === 1) yield {
162
164
  text: lines[i],
163
165
  width: require_displaywidth.getDisplayWidth(lines[i]),
164
- scopes
166
+ scopes,
167
+ ...marks
165
168
  };
166
169
  }
167
170
  return;
@@ -16,6 +16,11 @@ interface TerminalToken {
16
16
  readonly programBoundary?: boolean;
17
17
  /** Enables only the legacy default formatter whitespace behavior. */
18
18
  readonly legacyWhitespace?: boolean;
19
+ /**
20
+ * Leading whitespace only separates the token from preceding content, so
21
+ * message wrapping may drop it when the token starts a wrapped line.
22
+ */
23
+ readonly trimOnWrap?: boolean;
19
24
  }
20
25
  declare function terminalText(text: string): TerminalFragment;
21
26
  declare function formatTerminalTerm(term: TerminalTerm, theme?: TerminalTheme, quotes?: boolean, usage?: TerminalFormatContext["usage"]): TerminalFragment;
@@ -16,6 +16,11 @@ interface TerminalToken {
16
16
  readonly programBoundary?: boolean;
17
17
  /** Enables only the legacy default formatter whitespace behavior. */
18
18
  readonly legacyWhitespace?: boolean;
19
+ /**
20
+ * Leading whitespace only separates the token from preceding content, so
21
+ * message wrapping may drop it when the token starts a wrapped line.
22
+ */
23
+ readonly trimOnWrap?: boolean;
19
24
  }
20
25
  declare function terminalText(text: string): TerminalFragment;
21
26
  declare function formatTerminalTerm(term: TerminalTerm, theme?: TerminalTheme, quotes?: boolean, usage?: TerminalFormatContext["usage"]): TerminalFragment;
@@ -1,4 +1,5 @@
1
1
  import { getDisplayWidth } from "./displaywidth.js";
2
+ import { wrapTrimmableFragments } from "./terminal-separators.js";
2
3
  import { defaultTerminalTheme } from "./terminal.js";
3
4
 
4
5
  //#region src/terminal-internal.ts
@@ -151,6 +152,7 @@ function sameStyle(a, b) {
151
152
  }
152
153
  function* fragmentTokens(fragment, scopes = [], inherited = {}) {
153
154
  if (fragment.type === "text") {
155
+ const marks = wrapTrimmableFragments.has(fragment) ? { trimOnWrap: true } : {};
154
156
  const lines = fragment.text.split("\n");
155
157
  for (let i = 0; i < lines.length; i++) {
156
158
  if (i > 0) yield {
@@ -161,7 +163,8 @@ function* fragmentTokens(fragment, scopes = [], inherited = {}) {
161
163
  if (lines[i].length > 0 || lines.length === 1) yield {
162
164
  text: lines[i],
163
165
  width: getDisplayWidth(lines[i]),
164
- scopes
166
+ scopes,
167
+ ...marks
165
168
  };
166
169
  }
167
170
  return;
@@ -0,0 +1,13 @@
1
+
2
+ //#region src/terminal-separators.ts
3
+ /**
4
+ * Text fragments that only separate the items around them, such as the
5
+ * spaces the default `values` formatter puts between values. When message
6
+ * wrapping moves such a fragment to the start of a new line, its leading
7
+ * whitespace may be dropped.
8
+ * @internal
9
+ */
10
+ const wrapTrimmableFragments = /* @__PURE__ */ new WeakSet();
11
+
12
+ //#endregion
13
+ exports.wrapTrimmableFragments = wrapTrimmableFragments;
@@ -0,0 +1,12 @@
1
+ //#region src/terminal-separators.ts
2
+ /**
3
+ * Text fragments that only separate the items around them, such as the
4
+ * spaces the default `values` formatter puts between values. When message
5
+ * wrapping moves such a fragment to the start of a new line, its leading
6
+ * whitespace may be dropped.
7
+ * @internal
8
+ */
9
+ const wrapTrimmableFragments = /* @__PURE__ */ new WeakSet();
10
+
11
+ //#endregion
12
+ export { wrapTrimmableFragments };
package/dist/terminal.cjs CHANGED
@@ -1,3 +1,4 @@
1
+ const require_terminal_separators = require('./terminal-separators.cjs');
1
2
 
2
3
  //#region src/terminal.ts
3
4
  const plain = (text) => ({
@@ -9,6 +10,8 @@ const styled = (text, style) => ({
9
10
  style,
10
11
  children: [plain(text)]
11
12
  });
13
+ const valuesSeparator = plain(" ");
14
+ require_terminal_separators.wrapTrimmableFragments.add(valuesSeparator);
12
15
  /**
13
16
  * Optique's built-in terminal theme, with every formatter available for delegation.
14
17
  * @since 1.3.0
@@ -34,7 +37,7 @@ const defaultTerminalTheme = {
34
37
  value: (_term, ctx) => ctx.usage === "literal" ? plain(ctx.text) : styled(ctx.text, { foreground: "green" }),
35
38
  values: (term, ctx) => ({
36
39
  type: "concat",
37
- children: term.values.flatMap((value, i) => [...i === 0 ? [] : [plain(" ")], ctx.format({
40
+ children: term.values.flatMap((value, i) => [...i === 0 ? [] : [valuesSeparator], ctx.format({
38
41
  type: "value",
39
42
  value
40
43
  })])
package/dist/terminal.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { wrapTrimmableFragments } from "./terminal-separators.js";
2
+
1
3
  //#region src/terminal.ts
2
4
  const plain = (text) => ({
3
5
  type: "text",
@@ -8,6 +10,8 @@ const styled = (text, style) => ({
8
10
  style,
9
11
  children: [plain(text)]
10
12
  });
13
+ const valuesSeparator = plain(" ");
14
+ wrapTrimmableFragments.add(valuesSeparator);
11
15
  /**
12
16
  * Optique's built-in terminal theme, with every formatter available for delegation.
13
17
  * @since 1.3.0
@@ -33,7 +37,7 @@ const defaultTerminalTheme = {
33
37
  value: (_term, ctx) => ctx.usage === "literal" ? plain(ctx.text) : styled(ctx.text, { foreground: "green" }),
34
38
  values: (term, ctx) => ({
35
39
  type: "concat",
36
- children: term.values.flatMap((value, i) => [...i === 0 ? [] : [plain(" ")], ctx.format({
40
+ children: term.values.flatMap((value, i) => [...i === 0 ? [] : [valuesSeparator], ctx.format({
37
41
  type: "value",
38
42
  value
39
43
  })])
@@ -51,6 +51,18 @@ function placeText(text, cursor, maxWidth, trailingRoom = 0) {
51
51
  }
52
52
  };
53
53
  }
54
+ const leadingSpacePattern = /^((?:\x1B(?:\[[0-9;:]*[@-~]|\][^\x1B\x07]*(?:\x1B\\|\x07)))*)[^\S\n]+/;
55
+ /**
56
+ * Removes the whitespace that begins a text's first line, keeping any
57
+ * escape sequences before it. Used when layout moves text that starts with
58
+ * a separating space to a fresh line.
59
+ * @param text Rendered text, possibly containing ANSI styles or OSC links.
60
+ * @returns The text without its leading whitespace.
61
+ * @internal
62
+ */
63
+ function trimLeadingSpace(text) {
64
+ return text.replace(leadingSpacePattern, "$1");
65
+ }
54
66
  /**
55
67
  * Measures an annotation's fixed text without invoking theme callbacks.
56
68
  * @param prefix The already joined prefix and optional label.
@@ -84,4 +96,5 @@ function spaceAfterLabel(label) {
84
96
  exports.measureAnnotation = measureAnnotation;
85
97
  exports.measureText = measureText;
86
98
  exports.placeText = placeText;
87
- exports.spaceAfterLabel = spaceAfterLabel;
99
+ exports.spaceAfterLabel = spaceAfterLabel;
100
+ exports.trimLeadingSpace = trimLeadingSpace;
@@ -41,7 +41,14 @@ declare function placeText(text: string, cursor: TextCursor, maxWidth?: number,
41
41
  readonly text: string;
42
42
  readonly cursor: TextCursor;
43
43
  };
44
- /** Fixed annotation geometry shared by preflight and placement. @internal */
44
+ /**
45
+ * Removes the whitespace that begins a text's first line, keeping any
46
+ * escape sequences before it. Used when layout moves text that starts with
47
+ * a separating space to a fresh line.
48
+ * @param text Rendered text, possibly containing ANSI styles or OSC links.
49
+ * @returns The text without its leading whitespace.
50
+ * @internal
51
+ */
45
52
 
46
53
  /**
47
54
  * Adds a separating space only when a label occupies its final physical line.
@@ -41,7 +41,14 @@ declare function placeText(text: string, cursor: TextCursor, maxWidth?: number,
41
41
  readonly text: string;
42
42
  readonly cursor: TextCursor;
43
43
  };
44
- /** Fixed annotation geometry shared by preflight and placement. @internal */
44
+ /**
45
+ * Removes the whitespace that begins a text's first line, keeping any
46
+ * escape sequences before it. Used when layout moves text that starts with
47
+ * a separating space to a fresh line.
48
+ * @param text Rendered text, possibly containing ANSI styles or OSC links.
49
+ * @returns The text without its leading whitespace.
50
+ * @internal
51
+ */
45
52
 
46
53
  /**
47
54
  * Adds a separating space only when a label occupies its final physical line.
@@ -51,6 +51,18 @@ function placeText(text, cursor, maxWidth, trailingRoom = 0) {
51
51
  }
52
52
  };
53
53
  }
54
+ const leadingSpacePattern = /^((?:\x1B(?:\[[0-9;:]*[@-~]|\][^\x1B\x07]*(?:\x1B\\|\x07)))*)[^\S\n]+/;
55
+ /**
56
+ * Removes the whitespace that begins a text's first line, keeping any
57
+ * escape sequences before it. Used when layout moves text that starts with
58
+ * a separating space to a fresh line.
59
+ * @param text Rendered text, possibly containing ANSI styles or OSC links.
60
+ * @returns The text without its leading whitespace.
61
+ * @internal
62
+ */
63
+ function trimLeadingSpace(text) {
64
+ return text.replace(leadingSpacePattern, "$1");
65
+ }
54
66
  /**
55
67
  * Measures an annotation's fixed text without invoking theme callbacks.
56
68
  * @param prefix The already joined prefix and optional label.
@@ -81,4 +93,4 @@ function spaceAfterLabel(label) {
81
93
  }
82
94
 
83
95
  //#endregion
84
- export { measureAnnotation, measureText, placeText, spaceAfterLabel };
96
+ export { measureAnnotation, measureText, placeText, spaceAfterLabel, trimLeadingSpace };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optique/core",
3
- "version": "1.4.0-dev.2683",
3
+ "version": "1.4.0-dev.2708",
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.4.0-dev.2683+bacbadf3",
240
+ "@optique/env": "1.4.0-dev.2708+0119545d",
241
241
  "@types/node": "^24.0.0",
242
242
  "fast-check": "^4.7.0",
243
243
  "tsdown": "^0.13.0",