@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.
package/dist/facade.cjs CHANGED
@@ -1,8 +1,8 @@
1
1
  const require_internal_annotations = require('./internal/annotations.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
  const require_doc = require('./doc.cjs');
@@ -521,10 +521,10 @@ function handleCompletion(completionArgs, programName, parser, completionParser,
521
521
  messageFormatter,
522
522
  theme
523
523
  });
524
- const errorLabel = () => require_terminal_internal.renderTerminalTerm({
524
+ const errorLabel = () => require_text_layout.spaceAfterLabel(require_terminal_internal.renderTerminalTerm({
525
525
  type: "errorLabel",
526
526
  label: "Error:"
527
- }, theme, colors) + " ";
527
+ }, theme, colors));
528
528
  const shellName = completionArgs[0] || "";
529
529
  const args = completionArgs.slice(1);
530
530
  const callOnError = (code, error) => onError(code, error);
@@ -536,7 +536,7 @@ function handleCompletion(completionArgs, programName, parser, completionParser,
536
536
  colors,
537
537
  quotes: !colors,
538
538
  maxWidth,
539
- initialWidth: require_displaywidth.getDisplayWidth(prefix.split("\n").at(-1) ?? "")
539
+ initialWidth: require_text_layout.measureText(prefix).lastLineWidth
540
540
  })}\n`);
541
541
  if (completionParser) {
542
542
  const displayName = isOptionMode ? completionOptionDisplayName ?? "--completion" : completionCommandDisplayName ?? "completion";
@@ -570,7 +570,7 @@ function handleCompletion(completionArgs, programName, parser, completionParser,
570
570
  colors,
571
571
  quotes: !colors,
572
572
  maxWidth,
573
- initialWidth: require_displaywidth.getDisplayWidth(prefix.split("\n").at(-1) ?? "")
573
+ initialWidth: require_text_layout.measureText(prefix).lastLineWidth
574
574
  })}`);
575
575
  return require_mode_dispatch.dispatchByMode(parser.mode, () => {
576
576
  const result = callOnError(1, error);
@@ -886,17 +886,17 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
886
886
  messageFormatter,
887
887
  theme
888
888
  });
889
- const errorLabel = () => require_terminal_internal.renderTerminalTerm({
889
+ const errorLabel = () => require_text_layout.spaceAfterLabel(require_terminal_internal.renderTerminalTerm({
890
890
  type: "errorLabel",
891
891
  label: "Error:"
892
- }, theme, colors) + " ";
892
+ }, theme, colors));
893
893
  let usagePrefix;
894
- const usageLabel = () => usagePrefix ??= require_terminal_internal.renderTerminalTerm({
894
+ const usageLabel = () => usagePrefix ??= require_text_layout.spaceAfterLabel(require_terminal_internal.renderTerminalTerm({
895
895
  type: "label",
896
896
  label: "Usage:",
897
897
  kind: "usageSummary"
898
- }, theme, colors) + " ";
899
- const usageLabelWidth = () => require_displaywidth.getDisplayWidth(usageLabel().split("\n").at(-1) ?? "");
898
+ }, theme, colors));
899
+ const usageLabelWidth = () => require_text_layout.measureText(usageLabel()).lastLineWidth;
900
900
  const norm = (c) => c === true ? {} : c;
901
901
  const helpCommandConfig = norm(options.help?.command);
902
902
  const helpOptionConfig = norm(options.help?.option);
@@ -1059,7 +1059,7 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
1059
1059
  }), usageLabelWidth())}`);
1060
1060
  const prefix = errorLabel();
1061
1061
  const errorMessage = formatMessage(validationError, {
1062
- initialWidth: require_displaywidth.getDisplayWidth(prefix.split("\n").at(-1) ?? ""),
1062
+ initialWidth: require_text_layout.measureText(prefix).lastLineWidth,
1063
1063
  maxWidth,
1064
1064
  colors,
1065
1065
  quotes: !colors
@@ -1179,7 +1179,7 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
1179
1179
  }), usageLabelWidth())}`);
1180
1180
  const prefix = errorLabel();
1181
1181
  const errorMessage = formatMessage(classified.error, {
1182
- initialWidth: require_displaywidth.getDisplayWidth(prefix.split("\n").at(-1) ?? ""),
1182
+ initialWidth: require_text_layout.measureText(prefix).lastLineWidth,
1183
1183
  maxWidth,
1184
1184
  colors,
1185
1185
  quotes: !colors
package/dist/facade.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { injectAnnotations } from "./internal/annotations.js";
2
- import { getDisplayWidth } from "./displaywidth.js";
3
2
  import { renderTerminalTerm } from "./terminal-internal.js";
4
3
  import { commandLine, lineBreak, message, optionName, text, value } from "./message.js";
5
4
  import { resolveMessageFormatter } from "./message-renderer.js";
5
+ import { measureText, spaceAfterLabel } from "./text-layout.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";
@@ -521,10 +521,10 @@ function handleCompletion(completionArgs, programName, parser, completionParser,
521
521
  messageFormatter,
522
522
  theme
523
523
  });
524
- const errorLabel = () => renderTerminalTerm({
524
+ const errorLabel = () => spaceAfterLabel(renderTerminalTerm({
525
525
  type: "errorLabel",
526
526
  label: "Error:"
527
- }, theme, colors) + " ";
527
+ }, theme, colors));
528
528
  const shellName = completionArgs[0] || "";
529
529
  const args = completionArgs.slice(1);
530
530
  const callOnError = (code, error) => onError(code, error);
@@ -536,7 +536,7 @@ function handleCompletion(completionArgs, programName, parser, completionParser,
536
536
  colors,
537
537
  quotes: !colors,
538
538
  maxWidth,
539
- initialWidth: getDisplayWidth(prefix.split("\n").at(-1) ?? "")
539
+ initialWidth: measureText(prefix).lastLineWidth
540
540
  })}\n`);
541
541
  if (completionParser) {
542
542
  const displayName = isOptionMode ? completionOptionDisplayName ?? "--completion" : completionCommandDisplayName ?? "completion";
@@ -570,7 +570,7 @@ function handleCompletion(completionArgs, programName, parser, completionParser,
570
570
  colors,
571
571
  quotes: !colors,
572
572
  maxWidth,
573
- initialWidth: getDisplayWidth(prefix.split("\n").at(-1) ?? "")
573
+ initialWidth: measureText(prefix).lastLineWidth
574
574
  })}`);
575
575
  return dispatchByMode(parser.mode, () => {
576
576
  const result = callOnError(1, error);
@@ -886,17 +886,17 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
886
886
  messageFormatter,
887
887
  theme
888
888
  });
889
- const errorLabel = () => renderTerminalTerm({
889
+ const errorLabel = () => spaceAfterLabel(renderTerminalTerm({
890
890
  type: "errorLabel",
891
891
  label: "Error:"
892
- }, theme, colors) + " ";
892
+ }, theme, colors));
893
893
  let usagePrefix;
894
- const usageLabel = () => usagePrefix ??= renderTerminalTerm({
894
+ const usageLabel = () => usagePrefix ??= spaceAfterLabel(renderTerminalTerm({
895
895
  type: "label",
896
896
  label: "Usage:",
897
897
  kind: "usageSummary"
898
- }, theme, colors) + " ";
899
- const usageLabelWidth = () => getDisplayWidth(usageLabel().split("\n").at(-1) ?? "");
898
+ }, theme, colors));
899
+ const usageLabelWidth = () => measureText(usageLabel()).lastLineWidth;
900
900
  const norm = (c) => c === true ? {} : c;
901
901
  const helpCommandConfig = norm(options.help?.command);
902
902
  const helpOptionConfig = norm(options.help?.option);
@@ -1059,7 +1059,7 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
1059
1059
  }), usageLabelWidth())}`);
1060
1060
  const prefix = errorLabel();
1061
1061
  const errorMessage = formatMessage(validationError, {
1062
- initialWidth: getDisplayWidth(prefix.split("\n").at(-1) ?? ""),
1062
+ initialWidth: measureText(prefix).lastLineWidth,
1063
1063
  maxWidth,
1064
1064
  colors,
1065
1065
  quotes: !colors
@@ -1179,7 +1179,7 @@ function runParser(parserOrProgram, programNameOrArgs, argsOrOptions, optionsPar
1179
1179
  }), usageLabelWidth())}`);
1180
1180
  const prefix = errorLabel();
1181
1181
  const errorMessage = formatMessage(classified.error, {
1182
- initialWidth: getDisplayWidth(prefix.split("\n").at(-1) ?? ""),
1182
+ initialWidth: measureText(prefix).lastLineWidth,
1183
1183
  maxWidth,
1184
1184
  colors,
1185
1185
  quotes: !colors
@@ -1,7 +1,11 @@
1
1
  const require_displaywidth = require('../displaywidth.cjs');
2
2
  const require_terminal_internal = require('../terminal-internal.cjs');
3
3
  const require_message_renderer = require('../message-renderer.cjs');
4
+ const require_text_layout = require('../text-layout.cjs');
4
5
 
5
6
  exports.getDisplayWidth = require_displaywidth.getDisplayWidth;
7
+ exports.measureText = require_text_layout.measureText;
8
+ exports.placeText = require_text_layout.placeText;
6
9
  exports.renderTerminalTerm = require_terminal_internal.renderTerminalTerm;
7
- exports.resolveMessageFormatter = require_message_renderer.resolveMessageFormatter;
10
+ exports.resolveMessageFormatter = require_message_renderer.resolveMessageFormatter;
11
+ exports.spaceAfterLabel = require_text_layout.spaceAfterLabel;
@@ -1,4 +1,5 @@
1
1
  import { getDisplayWidth } from "../displaywidth.cjs";
2
2
  import { resolveMessageFormatter } from "../message-renderer.cjs";
3
3
  import { renderTerminalTerm } from "../terminal-internal.cjs";
4
- export { getDisplayWidth, renderTerminalTerm, resolveMessageFormatter };
4
+ import { measureText, placeText, spaceAfterLabel } from "../text-layout.cjs";
5
+ export { getDisplayWidth, measureText, placeText, renderTerminalTerm, resolveMessageFormatter, spaceAfterLabel };
@@ -1,4 +1,5 @@
1
1
  import { getDisplayWidth } from "../displaywidth.js";
2
2
  import { resolveMessageFormatter } from "../message-renderer.js";
3
3
  import { renderTerminalTerm } from "../terminal-internal.js";
4
- export { getDisplayWidth, renderTerminalTerm, resolveMessageFormatter };
4
+ import { measureText, placeText, spaceAfterLabel } from "../text-layout.js";
5
+ export { getDisplayWidth, measureText, placeText, renderTerminalTerm, resolveMessageFormatter, spaceAfterLabel };
@@ -1,5 +1,6 @@
1
1
  import { getDisplayWidth } from "../displaywidth.js";
2
2
  import { renderTerminalTerm } from "../terminal-internal.js";
3
3
  import { resolveMessageFormatter } from "../message-renderer.js";
4
+ import { measureText, placeText, spaceAfterLabel } from "../text-layout.js";
4
5
 
5
- export { getDisplayWidth, renderTerminalTerm, resolveMessageFormatter };
6
+ export { getDisplayWidth, measureText, placeText, renderTerminalTerm, resolveMessageFormatter, spaceAfterLabel };
@@ -127,11 +127,19 @@ function sameStyle(a, b) {
127
127
  }
128
128
  function* fragmentTokens(fragment, scopes = [], inherited = {}) {
129
129
  if (fragment.type === "text") {
130
- yield {
131
- text: fragment.text,
132
- width: require_displaywidth.getDisplayWidth(fragment.text),
133
- scopes
134
- };
130
+ const lines = fragment.text.split("\n");
131
+ for (let i = 0; i < lines.length; i++) {
132
+ if (i > 0) yield {
133
+ text: "\n",
134
+ width: -1,
135
+ scopes
136
+ };
137
+ if (lines[i].length > 0 || lines.length === 1) yield {
138
+ text: lines[i],
139
+ width: require_displaywidth.getDisplayWidth(lines[i]),
140
+ scopes
141
+ };
142
+ }
135
143
  return;
136
144
  }
137
145
  let next = scopes;
@@ -193,7 +201,7 @@ function serializeTokens(tokens, colors = false, resetSuffix = "", ambient = {})
193
201
  const items = [...tokens];
194
202
  for (let i = 0; i < items.length; i++) {
195
203
  const token = items[i];
196
- if (colors && token.width === -1) {
204
+ if (colors && token.width === -1 && token.scopes.length === 0) {
197
205
  const next = items.slice(i + 1).find((t) => t.width !== -1)?.scopes ?? [];
198
206
  let common = 0;
199
207
  while (common < active.length && common < next.length && active[common] === next[common]) common++;
@@ -10,6 +10,8 @@ interface TerminalToken {
10
10
  readonly text: string;
11
11
  readonly width: number;
12
12
  readonly scopes: readonly Scope[];
13
+ /** Layout-owned space, removable beside an explicit hard break. */
14
+ readonly separator?: boolean;
13
15
  }
14
16
  declare function terminalText(text: string): TerminalFragment;
15
17
  declare function formatTerminalTerm(term: TerminalTerm, theme?: TerminalTheme, quotes?: boolean, usage?: TerminalFormatContext["usage"]): TerminalFragment;
@@ -10,6 +10,8 @@ interface TerminalToken {
10
10
  readonly text: string;
11
11
  readonly width: number;
12
12
  readonly scopes: readonly Scope[];
13
+ /** Layout-owned space, removable beside an explicit hard break. */
14
+ readonly separator?: boolean;
13
15
  }
14
16
  declare function terminalText(text: string): TerminalFragment;
15
17
  declare function formatTerminalTerm(term: TerminalTerm, theme?: TerminalTheme, quotes?: boolean, usage?: TerminalFormatContext["usage"]): TerminalFragment;
@@ -127,11 +127,19 @@ function sameStyle(a, b) {
127
127
  }
128
128
  function* fragmentTokens(fragment, scopes = [], inherited = {}) {
129
129
  if (fragment.type === "text") {
130
- yield {
131
- text: fragment.text,
132
- width: getDisplayWidth(fragment.text),
133
- scopes
134
- };
130
+ const lines = fragment.text.split("\n");
131
+ for (let i = 0; i < lines.length; i++) {
132
+ if (i > 0) yield {
133
+ text: "\n",
134
+ width: -1,
135
+ scopes
136
+ };
137
+ if (lines[i].length > 0 || lines.length === 1) yield {
138
+ text: lines[i],
139
+ width: getDisplayWidth(lines[i]),
140
+ scopes
141
+ };
142
+ }
135
143
  return;
136
144
  }
137
145
  let next = scopes;
@@ -193,7 +201,7 @@ function serializeTokens(tokens, colors = false, resetSuffix = "", ambient = {})
193
201
  const items = [...tokens];
194
202
  for (let i = 0; i < items.length; i++) {
195
203
  const token = items[i];
196
- if (colors && token.width === -1) {
204
+ if (colors && token.width === -1 && token.scopes.length === 0) {
197
205
  const next = items.slice(i + 1).find((t) => t.width !== -1)?.scopes ?? [];
198
206
  let common = 0;
199
207
  while (common < active.length && common < next.length && active[common] === next[common]) common++;
@@ -61,7 +61,8 @@ interface TerminalStyle {
61
61
  readonly underline?: boolean;
62
62
  }
63
63
  /**
64
- * Structured output. Each text leaf is an indivisible wrapping unit.
64
+ * Structured output. Newlines in text leaves force line breaks; each line
65
+ * within a leaf is an indivisible wrapping unit.
65
66
  * @since 1.3.0
66
67
  */
67
68
  type TerminalFragment = {
@@ -61,7 +61,8 @@ interface TerminalStyle {
61
61
  readonly underline?: boolean;
62
62
  }
63
63
  /**
64
- * Structured output. Each text leaf is an indivisible wrapping unit.
64
+ * Structured output. Newlines in text leaves force line breaks; each line
65
+ * within a leaf is an indivisible wrapping unit.
65
66
  * @since 1.3.0
66
67
  */
67
68
  type TerminalFragment = {
@@ -0,0 +1,87 @@
1
+ const require_displaywidth = require('./displaywidth.cjs');
2
+
3
+ //#region src/text-layout.ts
4
+ /**
5
+ * Measures physical lines after removing terminal escape sequences.
6
+ * Empty text occupies one empty line; a trailing LF adds an empty final line.
7
+ * @param text Rendered text, possibly containing ANSI styles or OSC links.
8
+ * @returns First, last and widest line widths, and the number of lines.
9
+ * @internal
10
+ */
11
+ function measureText(text) {
12
+ const lines = require_displaywidth.stripAnsi(text).split("\n");
13
+ const widths = lines.map(require_displaywidth.getDisplayWidth);
14
+ return {
15
+ firstLineWidth: widths[0],
16
+ lastLineWidth: widths[widths.length - 1],
17
+ maxLineWidth: widths.reduce((widest, width) => Math.max(widest, width), 0),
18
+ lineCount: lines.length
19
+ };
20
+ }
21
+ /**
22
+ * Appends rendered text to a cursor, optionally moving it to a fresh line.
23
+ * Measures the actual join so combining sequences crossing the boundary are
24
+ * handled correctly. Hard breaks reset both the line and any external offset.
25
+ * This never reflows the text itself or changes its escape sequences.
26
+ * @param text Text to append.
27
+ * @param cursor The current line and column.
28
+ * @param maxWidth Optional column budget.
29
+ * @param trailingRoom Space to leave after a single-line block. An annotation
30
+ * prefix reserves one column for content; a multiline prefix already has a
31
+ * hard break, so its first line needs no reservation.
32
+ * @returns The placed text (possibly with a leading LF) and resulting cursor.
33
+ * @internal
34
+ */
35
+ function placeText(text, cursor, maxWidth, trailingRoom = 0) {
36
+ const plain = require_displaywidth.stripAnsi(text);
37
+ const lines = plain.split("\n");
38
+ const offset = cursor.column - require_displaywidth.getDisplayWidth(cursor.line);
39
+ const joinedWidth = offset + require_displaywidth.getDisplayWidth(cursor.line + lines[0]);
40
+ const room = lines.length === 1 ? trailingRoom : 0;
41
+ const breaks = maxWidth != null && (cursor.column > 0 || room > 0) && (joinedWidth > cursor.column || room > 0) && joinedWidth + room > maxWidth;
42
+ const lastLine = lines[lines.length - 1];
43
+ return {
44
+ text: breaks ? "\n" + text : text,
45
+ cursor: lines.length > 1 || breaks ? {
46
+ line: lastLine,
47
+ column: require_displaywidth.getDisplayWidth(lastLine)
48
+ } : {
49
+ line: cursor.line + plain,
50
+ column: joinedWidth
51
+ }
52
+ };
53
+ }
54
+ /**
55
+ * Measures an annotation's fixed text without invoking theme callbacks.
56
+ * @param prefix The already joined prefix and optional label.
57
+ * @param suffix The closing text.
58
+ * @returns Geometry reserving at least one content column after the suffix's
59
+ * first-line width has been deducted from the formatter budget.
60
+ * @internal
61
+ */
62
+ function measureAnnotation(prefix, suffix) {
63
+ const prefixMetrics = measureText(prefix);
64
+ const suffixMetrics = measureText(suffix);
65
+ return {
66
+ prefix,
67
+ suffix,
68
+ prefixMetrics,
69
+ suffixMetrics,
70
+ minWidth: Math.max(prefixMetrics.maxLineWidth, suffixMetrics.maxLineWidth, suffixMetrics.firstLineWidth + 1)
71
+ };
72
+ }
73
+ /**
74
+ * Adds a separating space only when a label occupies its final physical line.
75
+ * @param label The rendered label, including any styles and hard breaks.
76
+ * @returns The label with spacing for the text that follows it.
77
+ * @internal
78
+ */
79
+ function spaceAfterLabel(label) {
80
+ return measureText(label).lastLineWidth > 0 ? label + " " : label;
81
+ }
82
+
83
+ //#endregion
84
+ exports.measureAnnotation = measureAnnotation;
85
+ exports.measureText = measureText;
86
+ exports.placeText = placeText;
87
+ exports.spaceAfterLabel = spaceAfterLabel;
@@ -0,0 +1,54 @@
1
+ //#region src/text-layout.d.ts
2
+ /** Physical-line geometry at a rendered-text assembly boundary. @internal */
3
+ interface TextMetrics {
4
+ readonly firstLineWidth: number;
5
+ readonly lastLineWidth: number;
6
+ readonly maxLineWidth: number;
7
+ readonly lineCount: number;
8
+ }
9
+ /**
10
+ * Measures physical lines after removing terminal escape sequences.
11
+ * Empty text occupies one empty line; a trailing LF adds an empty final line.
12
+ * @param text Rendered text, possibly containing ANSI styles or OSC links.
13
+ * @returns First, last and widest line widths, and the number of lines.
14
+ * @internal
15
+ */
16
+ declare function measureText(text: string): TextMetrics;
17
+ /**
18
+ * The current visible line and its occupied column. The column may include
19
+ * an external offset for text unavailable to this renderer (initialWidth).
20
+ * @internal
21
+ */
22
+ interface TextCursor {
23
+ readonly line: string;
24
+ readonly column: number;
25
+ }
26
+ /**
27
+ * Appends rendered text to a cursor, optionally moving it to a fresh line.
28
+ * Measures the actual join so combining sequences crossing the boundary are
29
+ * handled correctly. Hard breaks reset both the line and any external offset.
30
+ * This never reflows the text itself or changes its escape sequences.
31
+ * @param text Text to append.
32
+ * @param cursor The current line and column.
33
+ * @param maxWidth Optional column budget.
34
+ * @param trailingRoom Space to leave after a single-line block. An annotation
35
+ * prefix reserves one column for content; a multiline prefix already has a
36
+ * hard break, so its first line needs no reservation.
37
+ * @returns The placed text (possibly with a leading LF) and resulting cursor.
38
+ * @internal
39
+ */
40
+ declare function placeText(text: string, cursor: TextCursor, maxWidth?: number, trailingRoom?: number): {
41
+ readonly text: string;
42
+ readonly cursor: TextCursor;
43
+ };
44
+ /** Fixed annotation geometry shared by preflight and placement. @internal */
45
+
46
+ /**
47
+ * Adds a separating space only when a label occupies its final physical line.
48
+ * @param label The rendered label, including any styles and hard breaks.
49
+ * @returns The label with spacing for the text that follows it.
50
+ * @internal
51
+ */
52
+ declare function spaceAfterLabel(label: string): string;
53
+ //#endregion
54
+ export { measureText, placeText, spaceAfterLabel };
@@ -0,0 +1,54 @@
1
+ //#region src/text-layout.d.ts
2
+ /** Physical-line geometry at a rendered-text assembly boundary. @internal */
3
+ interface TextMetrics {
4
+ readonly firstLineWidth: number;
5
+ readonly lastLineWidth: number;
6
+ readonly maxLineWidth: number;
7
+ readonly lineCount: number;
8
+ }
9
+ /**
10
+ * Measures physical lines after removing terminal escape sequences.
11
+ * Empty text occupies one empty line; a trailing LF adds an empty final line.
12
+ * @param text Rendered text, possibly containing ANSI styles or OSC links.
13
+ * @returns First, last and widest line widths, and the number of lines.
14
+ * @internal
15
+ */
16
+ declare function measureText(text: string): TextMetrics;
17
+ /**
18
+ * The current visible line and its occupied column. The column may include
19
+ * an external offset for text unavailable to this renderer (initialWidth).
20
+ * @internal
21
+ */
22
+ interface TextCursor {
23
+ readonly line: string;
24
+ readonly column: number;
25
+ }
26
+ /**
27
+ * Appends rendered text to a cursor, optionally moving it to a fresh line.
28
+ * Measures the actual join so combining sequences crossing the boundary are
29
+ * handled correctly. Hard breaks reset both the line and any external offset.
30
+ * This never reflows the text itself or changes its escape sequences.
31
+ * @param text Text to append.
32
+ * @param cursor The current line and column.
33
+ * @param maxWidth Optional column budget.
34
+ * @param trailingRoom Space to leave after a single-line block. An annotation
35
+ * prefix reserves one column for content; a multiline prefix already has a
36
+ * hard break, so its first line needs no reservation.
37
+ * @returns The placed text (possibly with a leading LF) and resulting cursor.
38
+ * @internal
39
+ */
40
+ declare function placeText(text: string, cursor: TextCursor, maxWidth?: number, trailingRoom?: number): {
41
+ readonly text: string;
42
+ readonly cursor: TextCursor;
43
+ };
44
+ /** Fixed annotation geometry shared by preflight and placement. @internal */
45
+
46
+ /**
47
+ * Adds a separating space only when a label occupies its final physical line.
48
+ * @param label The rendered label, including any styles and hard breaks.
49
+ * @returns The label with spacing for the text that follows it.
50
+ * @internal
51
+ */
52
+ declare function spaceAfterLabel(label: string): string;
53
+ //#endregion
54
+ export { measureText, placeText, spaceAfterLabel };
@@ -0,0 +1,84 @@
1
+ import { getDisplayWidth, stripAnsi } from "./displaywidth.js";
2
+
3
+ //#region src/text-layout.ts
4
+ /**
5
+ * Measures physical lines after removing terminal escape sequences.
6
+ * Empty text occupies one empty line; a trailing LF adds an empty final line.
7
+ * @param text Rendered text, possibly containing ANSI styles or OSC links.
8
+ * @returns First, last and widest line widths, and the number of lines.
9
+ * @internal
10
+ */
11
+ function measureText(text) {
12
+ const lines = stripAnsi(text).split("\n");
13
+ const widths = lines.map(getDisplayWidth);
14
+ return {
15
+ firstLineWidth: widths[0],
16
+ lastLineWidth: widths[widths.length - 1],
17
+ maxLineWidth: widths.reduce((widest, width) => Math.max(widest, width), 0),
18
+ lineCount: lines.length
19
+ };
20
+ }
21
+ /**
22
+ * Appends rendered text to a cursor, optionally moving it to a fresh line.
23
+ * Measures the actual join so combining sequences crossing the boundary are
24
+ * handled correctly. Hard breaks reset both the line and any external offset.
25
+ * This never reflows the text itself or changes its escape sequences.
26
+ * @param text Text to append.
27
+ * @param cursor The current line and column.
28
+ * @param maxWidth Optional column budget.
29
+ * @param trailingRoom Space to leave after a single-line block. An annotation
30
+ * prefix reserves one column for content; a multiline prefix already has a
31
+ * hard break, so its first line needs no reservation.
32
+ * @returns The placed text (possibly with a leading LF) and resulting cursor.
33
+ * @internal
34
+ */
35
+ function placeText(text, cursor, maxWidth, trailingRoom = 0) {
36
+ const plain = stripAnsi(text);
37
+ const lines = plain.split("\n");
38
+ const offset = cursor.column - getDisplayWidth(cursor.line);
39
+ const joinedWidth = offset + getDisplayWidth(cursor.line + lines[0]);
40
+ const room = lines.length === 1 ? trailingRoom : 0;
41
+ const breaks = maxWidth != null && (cursor.column > 0 || room > 0) && (joinedWidth > cursor.column || room > 0) && joinedWidth + room > maxWidth;
42
+ const lastLine = lines[lines.length - 1];
43
+ return {
44
+ text: breaks ? "\n" + text : text,
45
+ cursor: lines.length > 1 || breaks ? {
46
+ line: lastLine,
47
+ column: getDisplayWidth(lastLine)
48
+ } : {
49
+ line: cursor.line + plain,
50
+ column: joinedWidth
51
+ }
52
+ };
53
+ }
54
+ /**
55
+ * Measures an annotation's fixed text without invoking theme callbacks.
56
+ * @param prefix The already joined prefix and optional label.
57
+ * @param suffix The closing text.
58
+ * @returns Geometry reserving at least one content column after the suffix's
59
+ * first-line width has been deducted from the formatter budget.
60
+ * @internal
61
+ */
62
+ function measureAnnotation(prefix, suffix) {
63
+ const prefixMetrics = measureText(prefix);
64
+ const suffixMetrics = measureText(suffix);
65
+ return {
66
+ prefix,
67
+ suffix,
68
+ prefixMetrics,
69
+ suffixMetrics,
70
+ minWidth: Math.max(prefixMetrics.maxLineWidth, suffixMetrics.maxLineWidth, suffixMetrics.firstLineWidth + 1)
71
+ };
72
+ }
73
+ /**
74
+ * Adds a separating space only when a label occupies its final physical line.
75
+ * @param label The rendered label, including any styles and hard breaks.
76
+ * @returns The label with spacing for the text that follows it.
77
+ * @internal
78
+ */
79
+ function spaceAfterLabel(label) {
80
+ return measureText(label).lastLineWidth > 0 ? label + " " : label;
81
+ }
82
+
83
+ //#endregion
84
+ export { measureAnnotation, measureText, placeText, spaceAfterLabel };