patram 0.11.0 → 0.12.0

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.
Files changed (110) hide show
  1. package/bin/patram.js +4 -4
  2. package/lib/cli/commands/fields.js +0 -4
  3. package/lib/cli/commands/queries.js +10 -20
  4. package/lib/cli/commands/query.js +1 -8
  5. package/lib/cli/commands/refs.js +3 -10
  6. package/lib/cli/commands/show.js +1 -8
  7. package/lib/cli/help-metadata.js +71 -106
  8. package/lib/cli/main.js +10 -10
  9. package/lib/cli/parse-arguments-helpers.js +165 -59
  10. package/lib/cli/parse-arguments.js +4 -4
  11. package/lib/cli/render-help.js +2 -2
  12. package/lib/config/defaults.js +33 -25
  13. package/lib/config/load-patram-config.d.ts +8 -33
  14. package/lib/config/load-patram-config.js +9 -33
  15. package/lib/config/load-patram-config.types.d.ts +3 -40
  16. package/lib/config/manage-stored-queries-helpers.d.ts +4 -4
  17. package/lib/config/manage-stored-queries-helpers.js +91 -33
  18. package/lib/config/manage-stored-queries.d.ts +4 -4
  19. package/lib/config/manage-stored-queries.js +11 -5
  20. package/lib/config/patram-config.d.ts +34 -34
  21. package/lib/config/patram-config.js +3 -3
  22. package/lib/config/patram-config.types.d.ts +5 -11
  23. package/lib/config/resolve-patram-graph-config.d.ts +5 -1
  24. package/lib/config/resolve-patram-graph-config.js +3 -119
  25. package/lib/config/schema.d.ts +158 -269
  26. package/lib/config/schema.js +72 -210
  27. package/lib/config/validate-patram-config-value.js +6 -31
  28. package/lib/config/validation.d.ts +2 -12
  29. package/lib/config/validation.js +125 -483
  30. package/lib/find-close-match.d.ts +4 -1
  31. package/lib/graph/build-graph-identity.d.ts +1 -32
  32. package/lib/graph/build-graph-identity.js +5 -269
  33. package/lib/graph/build-graph.d.ts +13 -4
  34. package/lib/graph/build-graph.js +347 -488
  35. package/lib/graph/build-graph.types.d.ts +8 -9
  36. package/lib/graph/check-directive-metadata-helpers.d.ts +30 -0
  37. package/lib/graph/check-directive-metadata-helpers.js +126 -0
  38. package/lib/graph/check-directive-metadata.d.ts +8 -9
  39. package/lib/graph/check-directive-metadata.js +70 -561
  40. package/lib/graph/check-directive-path-target.d.ts +6 -13
  41. package/lib/graph/check-directive-path-target.js +26 -57
  42. package/lib/graph/check-directive-value.d.ts +1 -5
  43. package/lib/graph/check-directive-value.js +40 -180
  44. package/lib/graph/check-graph.d.ts +5 -5
  45. package/lib/graph/check-graph.js +8 -6
  46. package/lib/graph/document-node-identity.d.ts +23 -7
  47. package/lib/graph/document-node-identity.js +417 -160
  48. package/lib/graph/graph-node.d.ts +42 -0
  49. package/lib/graph/graph-node.js +83 -0
  50. package/lib/graph/inspect-reverse-references.js +16 -11
  51. package/lib/graph/load-project-graph.d.ts +7 -7
  52. package/lib/graph/load-project-graph.js +7 -7
  53. package/lib/graph/parse-where-clause.types.d.ts +3 -2
  54. package/lib/graph/query/cypher-reader.d.ts +59 -0
  55. package/lib/graph/query/cypher-reader.js +151 -0
  56. package/lib/graph/query/cypher-support.d.ts +79 -0
  57. package/lib/graph/query/cypher-support.js +213 -0
  58. package/lib/graph/query/cypher-tokenize.d.ts +13 -0
  59. package/lib/graph/query/cypher-tokenize.js +225 -0
  60. package/lib/graph/query/cypher.types.d.ts +43 -0
  61. package/lib/graph/query/execute.d.ts +7 -7
  62. package/lib/graph/query/execute.js +71 -33
  63. package/lib/graph/query/inspect.js +58 -24
  64. package/lib/graph/query/parse-cypher-patterns.d.ts +27 -0
  65. package/lib/graph/query/parse-cypher-patterns.js +382 -0
  66. package/lib/graph/query/parse-cypher.d.ts +7 -0
  67. package/lib/graph/query/parse-cypher.js +580 -0
  68. package/lib/graph/query/parse-query.d.ts +13 -0
  69. package/lib/graph/query/parse-query.js +97 -0
  70. package/lib/graph/query/resolve.js +77 -23
  71. package/lib/output/command-output.js +12 -5
  72. package/lib/output/compact-layout.js +221 -0
  73. package/lib/output/format-output-item-block.js +31 -1
  74. package/lib/output/format-output-metadata.js +16 -29
  75. package/lib/output/format-stored-query-block.js +95 -0
  76. package/lib/output/layout-incoming-references.js +101 -19
  77. package/lib/output/layout-stored-queries.js +23 -330
  78. package/lib/output/list-queries.js +1 -1
  79. package/lib/output/render-field-discovery.js +11 -2
  80. package/lib/output/render-output-view.js +9 -5
  81. package/lib/output/renderers/json.js +5 -26
  82. package/lib/output/renderers/plain.js +155 -35
  83. package/lib/output/renderers/rich.js +250 -36
  84. package/lib/output/resolved-link-layout.js +43 -0
  85. package/lib/output/rich-source/render.js +193 -35
  86. package/lib/output/show-document.js +25 -18
  87. package/lib/output/view-model/index.js +124 -103
  88. package/lib/parse/jsdoc/parse-jsdoc-blocks.js +1 -1
  89. package/lib/parse/jsdoc/parse-jsdoc-claims.js +12 -6
  90. package/lib/parse/markdown/parse-markdown-claims.js +99 -62
  91. package/lib/parse/markdown/parse-markdown-directives.d.ts +10 -6
  92. package/lib/parse/markdown/parse-markdown-directives.js +104 -18
  93. package/lib/parse/markdown/parse-markdown-prose.d.ts +27 -0
  94. package/lib/parse/markdown/parse-markdown-prose.js +243 -0
  95. package/lib/parse/parse-claims.d.ts +2 -6
  96. package/lib/parse/parse-claims.js +11 -53
  97. package/lib/parse/tagged-fenced/tagged-fenced-blocks.d.ts +4 -4
  98. package/lib/parse/tagged-fenced/tagged-fenced-blocks.js +4 -4
  99. package/lib/parse/yaml/parse-yaml-claims.js +4 -4
  100. package/lib/patram.d.ts +3 -5
  101. package/lib/patram.js +1 -1
  102. package/lib/scan/discover-fields.js +194 -55
  103. package/lib/scan/list-source-files.d.ts +4 -4
  104. package/lib/scan/list-source-files.js +4 -4
  105. package/package.json +1 -1
  106. package/lib/directive-validation-test-helpers.js +0 -87
  107. package/lib/graph/query/parse.d.ts +0 -75
  108. package/lib/graph/query/parse.js +0 -1064
  109. package/lib/output/derived-summary.js +0 -280
  110. package/lib/output/format-derived-summary-row.js +0 -9
@@ -17,28 +17,10 @@ import { findCloseMatch } from '../../find-close-match.js';
17
17
  * @returns {{ success: true, value: { query_source: QuerySource, where_clause: string } } | { error: CliParseError, success: false }}
18
18
  */
19
19
  export function resolveWhereClause(repo_config, command_arguments) {
20
- if (command_arguments[0] === '--where') {
21
- const where_clause = command_arguments.slice(1).join(' ').trim();
22
-
23
- if (where_clause.length === 0) {
24
- return {
25
- error: {
26
- code: 'message',
27
- message: 'Query requires a where clause.',
28
- },
29
- success: false,
30
- };
31
- }
20
+ const ad_hoc_result = resolveAdHocWhereClause(command_arguments);
32
21
 
33
- return {
34
- success: true,
35
- value: {
36
- query_source: {
37
- kind: 'ad_hoc',
38
- },
39
- where_clause,
40
- },
41
- };
22
+ if (ad_hoc_result) {
23
+ return ad_hoc_result;
42
24
  }
43
25
 
44
26
  const stored_query_name = command_arguments[0];
@@ -47,7 +29,7 @@ export function resolveWhereClause(repo_config, command_arguments) {
47
29
  return {
48
30
  error: {
49
31
  code: 'message',
50
- message: 'Query requires "--where" or a stored query name.',
32
+ message: 'Query requires "--cypher" or a stored query name.',
51
33
  },
52
34
  success: false,
53
35
  };
@@ -65,6 +47,18 @@ export function resolveWhereClause(repo_config, command_arguments) {
65
47
  };
66
48
  }
67
49
 
50
+ const query_text = stored_query.cypher;
51
+
52
+ if (!query_text) {
53
+ return {
54
+ error: {
55
+ code: 'message',
56
+ message: `Stored query "${stored_query_name}" is missing "cypher" query text.`,
57
+ },
58
+ success: false,
59
+ };
60
+ }
61
+
68
62
  return {
69
63
  success: true,
70
64
  value: {
@@ -72,7 +66,67 @@ export function resolveWhereClause(repo_config, command_arguments) {
72
66
  kind: 'stored_query',
73
67
  name: stored_query_name,
74
68
  },
75
- where_clause: stored_query.where,
69
+ where_clause: query_text,
70
+ },
71
+ };
72
+ }
73
+
74
+ /**
75
+ * @param {string[]} command_arguments
76
+ * @returns {{ success: true, value: { query_source: QuerySource, where_clause: string } } | { error: CliParseError, success: false } | null}
77
+ */
78
+ function resolveAdHocWhereClause(command_arguments) {
79
+ if (command_arguments[0] === '--cypher') {
80
+ return createAdHocWhereClauseResult(
81
+ command_arguments,
82
+ '--cypher',
83
+ 'Query requires a Cypher statement.',
84
+ );
85
+ }
86
+
87
+ if (command_arguments[0] === '--where') {
88
+ return {
89
+ error: {
90
+ code: 'message',
91
+ message: 'Query requires a Cypher statement.',
92
+ },
93
+ success: false,
94
+ };
95
+ }
96
+
97
+ return null;
98
+ }
99
+
100
+ /**
101
+ * @param {string[]} command_arguments
102
+ * @param {'--cypher'} option_name
103
+ * @param {string} empty_message
104
+ * @returns {{ success: true, value: { query_source: QuerySource, where_clause: string } } | { error: CliParseError, success: false }}
105
+ */
106
+ function createAdHocWhereClauseResult(
107
+ command_arguments,
108
+ option_name,
109
+ empty_message,
110
+ ) {
111
+ const query_text = command_arguments.slice(1).join(' ').trim();
112
+
113
+ if (query_text.length === 0) {
114
+ return {
115
+ error: {
116
+ code: 'message',
117
+ message: empty_message,
118
+ },
119
+ success: false,
120
+ };
121
+ }
122
+
123
+ return {
124
+ success: true,
125
+ value: {
126
+ query_source: {
127
+ kind: 'ad_hoc',
128
+ },
129
+ where_clause: query_text,
76
130
  },
77
131
  };
78
132
  }
@@ -15,17 +15,17 @@ import { writePagedOutput } from './write-paged-output.js';
15
15
  * Resolves the final output mode and switches between direct stdout writes and
16
16
  * interactive pager output.
17
17
  *
18
- * Kind: output
19
- * Status: active
20
- * Tracked in: ../../docs/plans/v0/source-anchor-dogfooding.md
21
- * Decided by: ../../docs/decisions/tty-pager-output.md
18
+ * kind: output
19
+ * status: active
20
+ * tracked_in: ../../docs/plans/v0/source-anchor-dogfooding.md
21
+ * decided_by: ../../docs/decisions/tty-pager-output.md
22
22
  * @patram
23
23
  * @see {@link ./render-output-view.js}
24
24
  * @see {@link ../../docs/decisions/tty-pager-output.md}
25
25
  */
26
26
 
27
27
  /**
28
- * @param {{ stdout: { isTTY?: boolean, write(chunk: string): boolean }, write_paged_output?: (output_text: string) => Promise<void> }} io_context
28
+ * @param {{ stdout: { columns?: number, isTTY?: boolean, write(chunk: string): boolean }, write_paged_output?: (output_text: string) => Promise<void> }} io_context
29
29
  * @param {ParsedCliArguments} parsed_command
30
30
  * @param {OutputView} output_view
31
31
  * @returns {Promise<void>}
@@ -43,6 +43,13 @@ export async function writeCommandOutput(
43
43
  term: process.env.TERM,
44
44
  }),
45
45
  parsed_command,
46
+ {
47
+ is_tty: io_context.stdout.isTTY === true,
48
+ terminal_width:
49
+ io_context.stdout.isTTY === true
50
+ ? io_context.stdout.columns
51
+ : undefined,
52
+ },
46
53
  );
47
54
 
48
55
  await writeRenderedCommandOutput(io_context, parsed_command, rendered_output);
@@ -0,0 +1,221 @@
1
+ import wrapAnsi from 'wrap-ansi';
2
+
3
+ const BODY_INDENT = ' ';
4
+ const TITLE_COLUMN_GAP = ' ';
5
+
6
+ /**
7
+ * @param {{ body_indent?: string, is_tty?: boolean, terminal_width?: number }} layout_options
8
+ * @returns {number}
9
+ */
10
+ function getCompactBodyWidth(layout_options = {}) {
11
+ const body_indent = layout_options.body_indent ?? BODY_INDENT;
12
+
13
+ if (!hasTtyWidth(layout_options)) {
14
+ return Number.POSITIVE_INFINITY;
15
+ }
16
+
17
+ const terminal_width = /** @type {number} */ (layout_options.terminal_width);
18
+
19
+ return Math.max(1, terminal_width - body_indent.length);
20
+ }
21
+
22
+ /**
23
+ * @param {string[]} left_titles
24
+ * @returns {number}
25
+ */
26
+ export function getCompactLeftTitleWidth(left_titles) {
27
+ if (left_titles.length === 0) {
28
+ return 0;
29
+ }
30
+
31
+ return Math.max(...left_titles.map((left_title) => left_title.length));
32
+ }
33
+
34
+ /**
35
+ * @param {string[]} metadata_rows
36
+ * @returns {string | undefined}
37
+ */
38
+ export function formatCompactMetadataLabel(metadata_rows) {
39
+ if (metadata_rows.length === 0) {
40
+ return undefined;
41
+ }
42
+
43
+ return `(${formatCompactMetadataFields(metadata_rows).join(', ')})`;
44
+ }
45
+
46
+ /**
47
+ * @param {{
48
+ * format_left?: (text: string) => string,
49
+ * format_right?: (text: string) => string,
50
+ * is_tty?: boolean,
51
+ * left_title: string,
52
+ * left_title_width: number,
53
+ * right_title?: string,
54
+ * terminal_width?: number,
55
+ * }} options
56
+ * @returns {string}
57
+ */
58
+ export function formatCompactTitleRow(options) {
59
+ const formatLeft = options.format_left ?? identity;
60
+ const formatRight = options.format_right ?? identity;
61
+
62
+ if (!options.right_title) {
63
+ return formatLeft(options.left_title);
64
+ }
65
+
66
+ if (!hasTtyWidth(options)) {
67
+ return `${formatLeft(options.left_title)}${TITLE_COLUMN_GAP}${formatRight(options.right_title)}`;
68
+ }
69
+
70
+ const terminal_width = /** @type {number} */ (options.terminal_width);
71
+ const available_width =
72
+ terminal_width - options.left_title.length - TITLE_COLUMN_GAP.length;
73
+
74
+ if (available_width <= 0) {
75
+ return formatLeft(options.left_title);
76
+ }
77
+
78
+ return `${formatLeft(options.left_title)}${TITLE_COLUMN_GAP}${formatRight(truncateText(options.right_title, available_width))}`;
79
+ }
80
+
81
+ /**
82
+ * @param {string} text
83
+ * @param {{ body_indent?: string, is_tty?: boolean, terminal_width?: number }} layout_options
84
+ * @returns {string[]}
85
+ */
86
+ export function wrapCompactBodyText(text, layout_options = {}) {
87
+ if (!hasTtyWidth(layout_options)) {
88
+ return text.split('\n');
89
+ }
90
+
91
+ const body_width = getCompactBodyWidth(layout_options);
92
+ /** @type {string[]} */
93
+ const lines = [];
94
+
95
+ for (const [line_index, source_line] of text.split('\n').entries()) {
96
+ if (line_index > 0 && source_line.length === 0) {
97
+ lines.push('');
98
+ continue;
99
+ }
100
+
101
+ const wrapped_line = wrapAnsi(source_line, body_width, {
102
+ hard: false,
103
+ trim: false,
104
+ wordWrap: true,
105
+ });
106
+
107
+ lines.push(...wrapped_line.split('\n'));
108
+ }
109
+
110
+ return lines;
111
+ }
112
+
113
+ /**
114
+ * @param {{ is_tty?: boolean, terminal_width?: number }} layout_options
115
+ * @returns {boolean}
116
+ */
117
+ function hasTtyWidth(layout_options) {
118
+ return (
119
+ layout_options.is_tty === true &&
120
+ typeof layout_options.terminal_width === 'number' &&
121
+ Number.isFinite(layout_options.terminal_width) &&
122
+ layout_options.terminal_width > 0
123
+ );
124
+ }
125
+
126
+ /**
127
+ * @param {string} value
128
+ * @param {number} max_width
129
+ * @returns {string}
130
+ */
131
+ function truncateText(value, max_width) {
132
+ if (value.length <= max_width) {
133
+ return value;
134
+ }
135
+
136
+ if (max_width <= 1) {
137
+ return '…';
138
+ }
139
+
140
+ if (hasEnclosingPunctuation(value, '(', ')')) {
141
+ return truncateEnclosedText(value, max_width, '(', ')');
142
+ }
143
+
144
+ if (hasEnclosingPunctuation(value, '[', ']')) {
145
+ return truncateEnclosedText(value, max_width, '[', ']');
146
+ }
147
+
148
+ return `${value.slice(0, max_width - 1)}…`;
149
+ }
150
+
151
+ /**
152
+ * @param {string} value
153
+ * @param {number} max_width
154
+ * @param {string} open_character
155
+ * @param {string} close_character
156
+ * @returns {string}
157
+ */
158
+ function truncateEnclosedText(
159
+ value,
160
+ max_width,
161
+ open_character,
162
+ close_character,
163
+ ) {
164
+ if (max_width === 2) {
165
+ return `…${close_character}`;
166
+ }
167
+
168
+ if (max_width === 3) {
169
+ return `${open_character}…${close_character}`;
170
+ }
171
+
172
+ const inner_value = value.slice(1, -1);
173
+ const prefix_width = Math.max(0, max_width - 3);
174
+
175
+ return `${open_character}${inner_value.slice(0, prefix_width)}…${close_character}`;
176
+ }
177
+
178
+ /**
179
+ * @param {string[]} metadata_rows
180
+ * @returns {string[]}
181
+ */
182
+ function formatCompactMetadataFields(metadata_rows) {
183
+ return metadata_rows
184
+ .flatMap((metadata_row) => metadata_row.split(' '))
185
+ .map(formatCompactMetadataField);
186
+ }
187
+
188
+ /**
189
+ * @param {string} metadata_field
190
+ * @returns {string}
191
+ */
192
+ function formatCompactMetadataField(metadata_field) {
193
+ const separator_index = metadata_field.indexOf(': ');
194
+
195
+ if (separator_index === -1) {
196
+ return metadata_field;
197
+ }
198
+
199
+ const field_name = metadata_field.slice(0, separator_index);
200
+ const field_value = metadata_field.slice(separator_index + 2);
201
+
202
+ return `${field_name}=${field_value}`;
203
+ }
204
+
205
+ /**
206
+ * @param {string} value
207
+ * @param {string} open_character
208
+ * @param {string} close_character
209
+ * @returns {boolean}
210
+ */
211
+ function hasEnclosingPunctuation(value, open_character, close_character) {
212
+ return value.startsWith(open_character) && value.endsWith(close_character);
213
+ }
214
+
215
+ /**
216
+ * @param {string} value
217
+ * @returns {string}
218
+ */
219
+ function identity(value) {
220
+ return value;
221
+ }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @param {{ header: string, metadata_rows: string[], metadata_indent?: string, title: string, title_indent?: string }} options
2
+ * @param {{ description?: string, header: string, metadata_rows: string[], metadata_indent?: string, title: string, title_indent?: string }} options
3
3
  * @returns {string}
4
4
  */
5
5
  export function formatOutputItemBlock(options) {
@@ -18,5 +18,35 @@ export function formatOutputItemBlock(options) {
18
18
 
19
19
  lines.push('', `${title_indent}${options.title}`);
20
20
 
21
+ if (options.description) {
22
+ lines.push(
23
+ '',
24
+ ...formatIndentedDescription(options.description, title_indent),
25
+ );
26
+ }
27
+
21
28
  return lines.join('\n');
22
29
  }
30
+
31
+ /**
32
+ * @param {string} description
33
+ * @param {string} indent
34
+ * @returns {string[]}
35
+ */
36
+ function formatIndentedDescription(description, indent) {
37
+ /** @type {string[]} */
38
+ const lines = [];
39
+ const paragraphs = description.split('\n\n');
40
+
41
+ for (const [paragraph_index, paragraph] of paragraphs.entries()) {
42
+ if (paragraph_index > 0) {
43
+ lines.push('');
44
+ }
45
+
46
+ for (const paragraph_line of paragraph.split('\n')) {
47
+ lines.push(`${indent}${paragraph_line}`);
48
+ }
49
+ }
50
+
51
+ return lines;
52
+ }
@@ -1,44 +1,31 @@
1
- import { formatDerivedSummaryRow } from './format-derived-summary-row.js';
2
-
3
1
  /**
4
2
  * @param {import('./output-view.types.ts').OutputNodeItem} output_item
5
- * @returns {string[]}
3
+ * @returns {string | undefined}
6
4
  */
7
- export function formatOutputNodeMetadataRows(output_item) {
8
- /** @type {string[]} */
9
- const metadata_rows = [];
10
- const stored_metadata_fields =
11
- output_item.visible_fields.map(formatMetadataField);
12
-
13
- if (stored_metadata_fields.length > 0) {
14
- metadata_rows.push(stored_metadata_fields.join(' '));
15
- }
16
-
17
- if (output_item.derived_summary) {
18
- metadata_rows.push(formatDerivedSummaryRow(output_item.derived_summary));
19
- }
20
-
21
- return metadata_rows;
5
+ export function formatOutputNodeStoredMetadataRow(output_item) {
6
+ return formatStoredMetadataRow(output_item.visible_fields);
22
7
  }
23
8
 
24
9
  /**
25
10
  * @param {import('./output-view.types.ts').OutputResolvedLinkTarget} target
26
- * @returns {string[]}
11
+ * @returns {string | undefined}
27
12
  */
28
- export function formatResolvedLinkMetadataRows(target) {
29
- /** @type {string[]} */
30
- const metadata_rows = [];
31
- const stored_metadata_fields = target.visible_fields.map(formatMetadataField);
13
+ export function formatResolvedLinkStoredMetadataRow(target) {
14
+ return formatStoredMetadataRow(target.visible_fields);
15
+ }
32
16
 
33
- if (stored_metadata_fields.length > 0) {
34
- metadata_rows.push(stored_metadata_fields.join(' '));
35
- }
17
+ /**
18
+ * @param {import('./output-view.types.ts').OutputMetadataField[]} visible_fields
19
+ * @returns {string | undefined}
20
+ */
21
+ function formatStoredMetadataRow(visible_fields) {
22
+ const stored_metadata_fields = visible_fields.map(formatMetadataField);
36
23
 
37
- if (target.derived_summary) {
38
- metadata_rows.push(formatDerivedSummaryRow(target.derived_summary));
24
+ if (stored_metadata_fields.length === 0) {
25
+ return undefined;
39
26
  }
40
27
 
41
- return metadata_rows;
28
+ return stored_metadata_fields.join(' ');
42
29
  }
43
30
 
44
31
  /**
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @import { OutputStoredQueryItem } from './output-view.types.ts';
3
+ */
4
+
5
+ import { wrapCompactBodyText } from './compact-layout.js';
6
+ import { layoutStoredQueryRow } from './layout-stored-queries.js';
7
+
8
+ /**
9
+ * @typedef {{
10
+ * kind: 'description' | 'field_name' | 'keyword' | 'literal' | 'name' | 'operator' | 'plain',
11
+ * text: string,
12
+ * }} StoredQueryLineSegment
13
+ */
14
+
15
+ /**
16
+ * @param {OutputStoredQueryItem} output_item
17
+ * @param {{ is_tty?: boolean, terminal_width?: number }} render_options
18
+ * @param {{
19
+ * format_description?: (text: string) => string,
20
+ * format_line: (line_segments: StoredQueryLineSegment[]) => string,
21
+ * format_name: (text: string) => string,
22
+ * }} format_options
23
+ * @returns {string}
24
+ */
25
+ export function formatStoredQueryBlock(
26
+ output_item,
27
+ render_options,
28
+ format_options,
29
+ ) {
30
+ const row_layout = getStoredQueryRowLayout(output_item.name, render_options);
31
+ const formatDescription = format_options.format_description ?? identity;
32
+ const output_lines = layoutStoredQueryRow(
33
+ output_item.where,
34
+ row_layout.first_line_width,
35
+ row_layout.continuation_width,
36
+ ).map((line_segments, line_index) =>
37
+ line_index === 0
38
+ ? `${format_options.format_name(output_item.name)} ${format_options.format_line(line_segments)}`
39
+ : `${row_layout.continuation_prefix}${format_options.format_line(line_segments)}`,
40
+ );
41
+
42
+ if (output_item.description) {
43
+ for (const description_line of wrapCompactBodyText(
44
+ output_item.description,
45
+ render_options,
46
+ )) {
47
+ output_lines.push(
48
+ description_line.length > 0
49
+ ? ` ${formatDescription(description_line)}`
50
+ : '',
51
+ );
52
+ }
53
+ }
54
+
55
+ return output_lines.join('\n');
56
+ }
57
+
58
+ /**
59
+ * @param {string} query_name
60
+ * @param {{ is_tty?: boolean, terminal_width?: number }} render_options
61
+ * @returns {{ continuation_prefix: string, continuation_width: number, first_line_width: number }}
62
+ */
63
+ function getStoredQueryRowLayout(query_name, render_options) {
64
+ const continuation_prefix = ' '.repeat(query_name.length + 2);
65
+
66
+ if (
67
+ render_options.is_tty !== true ||
68
+ typeof render_options.terminal_width !== 'number'
69
+ ) {
70
+ return {
71
+ continuation_prefix,
72
+ continuation_width: Number.POSITIVE_INFINITY,
73
+ first_line_width: Number.POSITIVE_INFINITY,
74
+ };
75
+ }
76
+
77
+ const available_width = Math.max(
78
+ 1,
79
+ render_options.terminal_width - continuation_prefix.length,
80
+ );
81
+
82
+ return {
83
+ continuation_prefix,
84
+ continuation_width: available_width,
85
+ first_line_width: available_width,
86
+ };
87
+ }
88
+
89
+ /**
90
+ * @param {string} value
91
+ * @returns {string}
92
+ */
93
+ function identity(value) {
94
+ return value;
95
+ }