patram 0.1.1 → 0.3.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 (49) hide show
  1. package/lib/build-graph-identity.js +57 -24
  2. package/lib/build-graph.js +383 -17
  3. package/lib/build-graph.types.ts +5 -2
  4. package/lib/check-directive-metadata.js +516 -0
  5. package/lib/check-directive-value.js +282 -0
  6. package/lib/check-graph.js +24 -5
  7. package/lib/cli-help-metadata.js +580 -0
  8. package/lib/derived-summary.js +280 -0
  9. package/lib/directive-diagnostics.js +38 -0
  10. package/lib/directive-type-rules.js +133 -0
  11. package/lib/discover-fields.js +427 -0
  12. package/lib/discover-fields.types.ts +52 -0
  13. package/lib/format-derived-summary-row.js +9 -0
  14. package/lib/format-node-header.js +21 -0
  15. package/lib/format-output-item-block.js +22 -0
  16. package/lib/format-output-metadata.js +54 -0
  17. package/lib/layout-stored-queries.js +96 -2
  18. package/lib/load-patram-config.js +754 -18
  19. package/lib/load-patram-config.types.ts +128 -2
  20. package/lib/load-project-graph.js +4 -1
  21. package/lib/output-view.types.ts +29 -6
  22. package/lib/parse-cli-arguments-helpers.js +263 -90
  23. package/lib/parse-cli-arguments.js +160 -8
  24. package/lib/parse-cli-arguments.types.ts +49 -4
  25. package/lib/parse-where-clause.js +670 -209
  26. package/lib/parse-where-clause.types.ts +72 -0
  27. package/lib/patram-cli.js +180 -21
  28. package/lib/patram-config.js +31 -31
  29. package/lib/patram-config.types.ts +10 -4
  30. package/lib/patram.js +6 -0
  31. package/lib/query-graph.js +444 -113
  32. package/lib/query-inspection.js +798 -0
  33. package/lib/render-check-output.js +1 -1
  34. package/lib/render-cli-help.js +419 -0
  35. package/lib/render-field-discovery.js +148 -0
  36. package/lib/render-json-output.js +66 -14
  37. package/lib/render-output-view.js +272 -22
  38. package/lib/render-plain-output.js +31 -86
  39. package/lib/render-rich-output.js +34 -87
  40. package/lib/resolve-patram-graph-config.js +15 -9
  41. package/lib/resolve-where-clause.js +18 -3
  42. package/lib/show-document.js +51 -7
  43. package/lib/tagged-fenced-block-error.js +17 -0
  44. package/lib/tagged-fenced-block-markdown.js +111 -0
  45. package/lib/tagged-fenced-block-metadata.js +97 -0
  46. package/lib/tagged-fenced-block-parser.js +292 -0
  47. package/lib/tagged-fenced-blocks.js +100 -0
  48. package/lib/tagged-fenced-blocks.types.ts +38 -0
  49. package/package.json +12 -7
@@ -1,8 +1,11 @@
1
1
  /** @import * as $k$$l$output$j$view$k$types$k$ts from './output-view.types.ts'; */
2
+ /* eslint-disable max-lines */
2
3
  /**
3
- * @import { GraphNode } from './build-graph.types.ts';
4
+ * @import { BuildGraphResult, GraphNode } from './build-graph.types.ts';
5
+ * @import { DerivedSummaryEvaluator } from './derived-summary.js';
6
+ * @import { PatramRepoConfig } from './load-patram-config.types.ts';
4
7
  * @import { ParsedCliArguments } from './parse-cli-arguments.types.ts';
5
- * @import { OutputStoredQueryItem, OutputView, ResolvedOutputMode, ShowOutputView } from './output-view.types.ts';
8
+ * @import { OutputMetadataField, OutputStoredQueryItem, OutputView, ResolvedOutputMode, ShowOutputView } from './output-view.types.ts';
6
9
  */
7
10
 
8
11
  import { renderJsonOutput } from './render-json-output.js';
@@ -29,7 +32,7 @@ import { renderRichOutput } from './render-rich-output.js';
29
32
  *
30
33
  * @param {'query' | 'queries'} command_name
31
34
  * @param {GraphNode[] | { name: string, where: string }[]} command_items
32
- * @param {{ hints?: string[], limit?: number, offset?: number, total_count?: number }=} command_options
35
+ * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, hints?: string[], limit?: number, offset?: number, repo_config?: PatramRepoConfig, total_count?: number }=} command_options
33
36
  * @returns {OutputView}
34
37
  */
35
38
  export function createOutputView(command_name, command_items, command_options) {
@@ -53,17 +56,38 @@ export function createOutputView(command_name, command_items, command_options) {
53
56
  * Create a shared output view for the show command.
54
57
  *
55
58
  * @param {{ path: string, rendered_source: string, resolved_links: Array<{ label: string, reference: number, target: { kind?: string, path: string, status?: string, title: string } }>, source: string }} show_output
59
+ * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, graph_nodes?: BuildGraphResult['nodes'], repo_config?: PatramRepoConfig }=} command_options
56
60
  * @returns {ShowOutputView}
57
61
  */
58
- export function createShowOutputView(show_output) {
62
+ export function createShowOutputView(show_output, command_options = {}) {
63
+ const shown_document_node =
64
+ command_options.graph_nodes?.[`doc:${show_output.path}`];
65
+
59
66
  return {
60
67
  command: 'show',
68
+ document: shown_document_node
69
+ ? createOutputNodeItem(
70
+ shown_document_node,
71
+ command_options.derived_summary_evaluator?.evaluate(
72
+ shown_document_node,
73
+ ) ?? null,
74
+ command_options.repo_config?.fields ?? {},
75
+ )
76
+ : undefined,
61
77
  hints: [],
62
78
  items: show_output.resolved_links.map((resolved_link) => ({
63
79
  kind: 'resolved_link',
64
80
  label: resolved_link.label,
65
81
  reference: resolved_link.reference,
66
- target: createResolvedLinkTarget(resolved_link.target),
82
+ target: createResolvedLinkTarget(
83
+ resolved_link.target,
84
+ command_options.repo_config?.fields ?? {},
85
+ command_options.graph_nodes?.[`doc:${resolved_link.target.path}`]
86
+ ? (command_options.derived_summary_evaluator?.evaluate(
87
+ command_options.graph_nodes[`doc:${resolved_link.target.path}`],
88
+ ) ?? null)
89
+ : null,
90
+ ),
67
91
  })),
68
92
  path: show_output.path,
69
93
  rendered_source: show_output.rendered_source,
@@ -104,7 +128,7 @@ export async function renderOutputView(
104
128
 
105
129
  /**
106
130
  * @param {GraphNode[]} graph_nodes
107
- * @param {{ hints?: string[], limit?: number, offset?: number, total_count?: number }=} command_options
131
+ * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, hints?: string[], limit?: number, offset?: number, repo_config?: PatramRepoConfig, total_count?: number }=} command_options
108
132
  * @returns {OutputView}
109
133
  */
110
134
  function createQueryOutputView(graph_nodes, command_options = {}) {
@@ -114,8 +138,14 @@ function createQueryOutputView(graph_nodes, command_options = {}) {
114
138
  command: 'query',
115
139
  hints:
116
140
  command_options.hints ??
117
- (total_count === 0 ? ['Try: patram query --where "kind=task"'] : []),
118
- items: graph_nodes.map(createOutputNodeItem),
141
+ (total_count === 0 ? ['Try: patram query --where "$class=task"'] : []),
142
+ items: graph_nodes.map((graph_node) =>
143
+ createOutputNodeItem(
144
+ graph_node,
145
+ command_options.derived_summary_evaluator?.evaluate(graph_node) ?? null,
146
+ command_options.repo_config?.fields ?? {},
147
+ ),
148
+ ),
119
149
  summary: {
120
150
  count: graph_nodes.length,
121
151
  kind: 'result_list',
@@ -148,46 +178,266 @@ function createStoredQueriesOutputView(stored_queries) {
148
178
 
149
179
  /**
150
180
  * @param {GraphNode} graph_node
181
+ * @param {import('./output-view.types.ts').OutputDerivedSummary | null} derived_summary
182
+ * @param {NonNullable<PatramRepoConfig['fields']>} field_definitions
151
183
  * @returns {$k$$l$output$j$view$k$types$k$ts.OutputNodeItem}
152
184
  */
153
- function createOutputNodeItem(graph_node) {
154
- const title =
155
- graph_node.title ?? graph_node.label ?? graph_node.path ?? graph_node.key;
185
+ function createOutputNodeItem(graph_node, derived_summary, field_definitions) {
186
+ const title = getOutputNodeTitle(graph_node);
187
+ const path = getOutputNodePath(graph_node);
188
+ const node_class = getOutputNodeClass(graph_node);
189
+ const fields = collectOutputFields(graph_node, field_definitions);
190
+ const visible_fields = createVisibleOutputFields(fields, field_definitions);
156
191
 
157
- if (!title || !graph_node.path) {
192
+ if (!title || !node_class) {
158
193
  throw new Error(
159
194
  `Expected graph node "${graph_node.id}" to have a title and path.`,
160
195
  );
161
196
  }
162
197
 
163
198
  return {
164
- id: graph_node.id,
199
+ derived_summary: derived_summary ?? undefined,
200
+ fields,
201
+ id: getOutputNodeId(graph_node),
165
202
  kind: 'node',
166
- node_kind: graph_node.kind,
167
- path: graph_node.path,
168
- status: graph_node.status,
203
+ node_kind: node_class,
204
+ path,
169
205
  title,
206
+ visible_fields,
170
207
  };
171
208
  }
172
209
 
173
210
  /**
174
211
  * @param {{ kind?: string, path: string, status?: string, title: string }} target
212
+ * @param {NonNullable<PatramRepoConfig['fields']>} field_definitions
213
+ * @param {import('./output-view.types.ts').OutputDerivedSummary | null} derived_summary
175
214
  * @returns {$k$$l$output$j$view$k$types$k$ts.OutputResolvedLinkTarget}
176
215
  */
177
- function createResolvedLinkTarget(target) {
216
+ function createResolvedLinkTarget(target, field_definitions, derived_summary) {
217
+ /** @type {Record<string, string | string[]>} */
218
+ const fields = {};
219
+
220
+ if (target.status) {
221
+ fields.status = target.status;
222
+ }
223
+
178
224
  /** @type {$k$$l$output$j$view$k$types$k$ts.OutputResolvedLinkTarget} */
179
225
  const resolved_target = {
226
+ derived_summary: derived_summary ?? undefined,
227
+ fields,
228
+ id: `doc:${target.path}`,
229
+ kind: target.kind ?? 'document',
180
230
  path: target.path,
181
231
  title: target.title,
232
+ visible_fields: createVisibleOutputFields(fields, field_definitions),
182
233
  };
183
234
 
184
- if (target.kind && target.kind !== 'document') {
185
- resolved_target.kind = target.kind;
235
+ return resolved_target;
236
+ }
237
+
238
+ /**
239
+ * @param {string | string[] | undefined} field_value
240
+ * @returns {string | undefined}
241
+ */
242
+ function getScalarGraphNodeField(field_value) {
243
+ if (Array.isArray(field_value)) {
244
+ return field_value[0];
186
245
  }
187
246
 
188
- if (target.status) {
189
- resolved_target.status = target.status;
247
+ return field_value;
248
+ }
249
+
250
+ /**
251
+ * @param {GraphNode} graph_node
252
+ * @returns {string | undefined}
253
+ */
254
+ function getOutputNodeTitle(graph_node) {
255
+ return (
256
+ getScalarGraphNodeField(graph_node.title) ??
257
+ getScalarGraphNodeField(graph_node.label) ??
258
+ getOutputNodePath(graph_node) ??
259
+ getScalarGraphNodeField(graph_node.key)
260
+ );
261
+ }
262
+
263
+ /**
264
+ * @param {GraphNode} graph_node
265
+ * @returns {string | undefined}
266
+ */
267
+ function getOutputNodePath(graph_node) {
268
+ return getScalarGraphNodeField(graph_node.$path ?? graph_node.path);
269
+ }
270
+
271
+ /**
272
+ * @param {GraphNode} graph_node
273
+ * @returns {string | undefined}
274
+ */
275
+ function getOutputNodeClass(graph_node) {
276
+ return getScalarGraphNodeField(graph_node.$class ?? graph_node.kind);
277
+ }
278
+
279
+ /**
280
+ * @param {GraphNode} graph_node
281
+ * @returns {string}
282
+ */
283
+ function getOutputNodeId(graph_node) {
284
+ return (
285
+ getScalarGraphNodeField(graph_node.$id ?? graph_node.id) ?? graph_node.id
286
+ );
287
+ }
288
+
289
+ /**
290
+ * @param {GraphNode} graph_node
291
+ * @param {NonNullable<PatramRepoConfig['fields']>} field_definitions
292
+ * @returns {Record<string, string | string[]>}
293
+ */
294
+ function collectOutputFields(graph_node, field_definitions) {
295
+ /** @type {Record<string, string | string[]>} */
296
+ const fields = {};
297
+
298
+ for (const [field_name, field_value] of Object.entries(graph_node)) {
299
+ const normalized_value = getCollectedOutputFieldValue(
300
+ graph_node,
301
+ field_name,
302
+ field_value,
303
+ );
304
+
305
+ if (normalized_value === undefined) {
306
+ continue;
307
+ }
308
+
309
+ fields[field_name] = normalized_value;
190
310
  }
191
311
 
192
- return resolved_target;
312
+ for (const field_name of Object.keys(field_definitions)) {
313
+ if (fields[field_name] !== undefined) {
314
+ continue;
315
+ }
316
+
317
+ const field_value = normalizeOutputFieldValue(graph_node[field_name]);
318
+
319
+ if (field_value !== undefined) {
320
+ fields[field_name] = field_value;
321
+ }
322
+ }
323
+
324
+ return fields;
325
+ }
326
+
327
+ /**
328
+ * @param {GraphNode} graph_node
329
+ * @param {string} field_name
330
+ * @param {unknown} field_value
331
+ * @returns {string | string[] | undefined}
332
+ */
333
+ function getCollectedOutputFieldValue(graph_node, field_name, field_value) {
334
+ if (isInternalOutputField(field_name)) {
335
+ return undefined;
336
+ }
337
+
338
+ const normalized_value = normalizeOutputFieldValue(field_value);
339
+
340
+ if (normalized_value === undefined) {
341
+ return undefined;
342
+ }
343
+
344
+ if (isLegacyMirrorOutputField(graph_node, field_name, normalized_value)) {
345
+ return undefined;
346
+ }
347
+
348
+ return normalized_value;
349
+ }
350
+
351
+ /**
352
+ * @param {GraphNode} graph_node
353
+ * @param {string} field_name
354
+ * @param {string | string[]} normalized_value
355
+ * @returns {boolean}
356
+ */
357
+ function isLegacyMirrorOutputField(graph_node, field_name, normalized_value) {
358
+ if (Array.isArray(normalized_value)) {
359
+ return false;
360
+ }
361
+
362
+ if (field_name === 'kind') {
363
+ return normalized_value === graph_node.$class;
364
+ }
365
+
366
+ if (field_name === 'path') {
367
+ return normalized_value === graph_node.$path;
368
+ }
369
+
370
+ if (field_name === 'id') {
371
+ return normalized_value === graph_node.$id;
372
+ }
373
+
374
+ return false;
375
+ }
376
+
377
+ /**
378
+ * @param {Record<string, string | string[]>} fields
379
+ * @param {NonNullable<PatramRepoConfig['fields']>} field_definitions
380
+ * @returns {OutputMetadataField[]}
381
+ */
382
+ function createVisibleOutputFields(fields, field_definitions) {
383
+ return Object.entries(fields)
384
+ .filter(
385
+ ([field_name]) => field_definitions[field_name]?.display?.hidden !== true,
386
+ )
387
+ .sort(([left_name], [right_name]) =>
388
+ compareOutputFieldNames(left_name, right_name, field_definitions),
389
+ )
390
+ .map(([name, value]) => ({ name, value }));
391
+ }
392
+
393
+ /**
394
+ * @param {string} field_name
395
+ * @returns {boolean}
396
+ */
397
+ function isInternalOutputField(field_name) {
398
+ return (
399
+ field_name === '$class' ||
400
+ field_name === '$id' ||
401
+ field_name === '$path' ||
402
+ field_name === 'id' ||
403
+ field_name === 'key' ||
404
+ field_name === 'label' ||
405
+ field_name === 'path' ||
406
+ field_name === 'title'
407
+ );
408
+ }
409
+
410
+ /**
411
+ * @param {string} left_name
412
+ * @param {string} right_name
413
+ * @param {NonNullable<PatramRepoConfig['fields']>} field_definitions
414
+ * @returns {number}
415
+ */
416
+ function compareOutputFieldNames(left_name, right_name, field_definitions) {
417
+ const left_order =
418
+ field_definitions[left_name]?.display?.order ?? Number.MAX_SAFE_INTEGER;
419
+ const right_order =
420
+ field_definitions[right_name]?.display?.order ?? Number.MAX_SAFE_INTEGER;
421
+
422
+ if (left_order !== right_order) {
423
+ return left_order - right_order;
424
+ }
425
+
426
+ return left_name.localeCompare(right_name, 'en');
427
+ }
428
+
429
+ /**
430
+ * @param {unknown} field_value
431
+ * @returns {string | string[] | undefined}
432
+ */
433
+ function normalizeOutputFieldValue(field_value) {
434
+ if (Array.isArray(field_value)) {
435
+ const string_values = field_value.flatMap((value) =>
436
+ typeof value === 'string' ? [value] : [],
437
+ );
438
+
439
+ return string_values.length > 0 ? string_values : undefined;
440
+ }
441
+
442
+ return typeof field_value === 'string' ? field_value : undefined;
193
443
  }
@@ -2,6 +2,12 @@
2
2
  * @import { OutputNodeItem, OutputResolvedLinkItem, OutputStoredQueryItem, OutputView, QueryOutputView, ShowOutputView } from './output-view.types.ts';
3
3
  */
4
4
 
5
+ import {
6
+ formatOutputNodeMetadataRows,
7
+ formatResolvedLinkMetadataRows,
8
+ } from './format-output-metadata.js';
9
+ import { formatNodeHeader } from './format-node-header.js';
10
+ import { formatOutputItemBlock } from './format-output-item-block.js';
5
11
  import { layoutStoredQueries } from './layout-stored-queries.js';
6
12
 
7
13
  /**
@@ -76,12 +82,24 @@ function renderPlainStoredQueries(output_items) {
76
82
  */
77
83
  function renderPlainShowOutput(output_view) {
78
84
  const rendered_source = trimTrailingLineBreaks(output_view.rendered_source);
85
+ const document_summary = output_view.document
86
+ ? formatPlainNodeItem(output_view.document)
87
+ : '';
79
88
 
80
- if (output_view.items.length === 0) {
89
+ if (document_summary.length === 0 && output_view.items.length === 0) {
81
90
  return `${rendered_source}\n`;
82
91
  }
83
92
 
84
- return `${rendered_source}\n\n----------------\n${output_view.items.map(formatPlainResolvedLinkItem).join('\n\n')}\n`;
93
+ /** @type {string[]} */
94
+ const summary_items = [];
95
+
96
+ if (document_summary.length > 0) {
97
+ summary_items.push(document_summary);
98
+ }
99
+
100
+ summary_items.push(...output_view.items.map(formatPlainResolvedLinkItem));
101
+
102
+ return `${rendered_source}\n\n----------------\n${summary_items.join('\n\n')}\n`;
85
103
  }
86
104
 
87
105
  /**
@@ -89,17 +107,11 @@ function renderPlainShowOutput(output_view) {
89
107
  * @returns {string}
90
108
  */
91
109
  function formatPlainNodeItem(output_item) {
92
- const metadata_row = formatMetadataRow(output_item);
93
- /** @type {string[]} */
94
- const lines = [formatNodeHeader(output_item)];
95
-
96
- if (metadata_row.length > 0) {
97
- lines.push(metadata_row);
98
- }
99
-
100
- lines.push('', ` ${output_item.title}`);
101
-
102
- return lines.join('\n');
110
+ return formatOutputItemBlock({
111
+ header: formatNodeHeader(output_item),
112
+ metadata_rows: formatOutputNodeMetadataRows(output_item),
113
+ title: output_item.title,
114
+ });
103
115
  }
104
116
 
105
117
  /**
@@ -115,79 +127,12 @@ function formatPlainStoredQueryLine(line_segments) {
115
127
  * @returns {string}
116
128
  */
117
129
  function formatPlainResolvedLinkItem(output_item) {
118
- const metadata_row = formatResolvedLinkMetadataRow(output_item.target);
119
- /** @type {string[]} */
120
- const lines = [
121
- `[${output_item.reference}] document ${output_item.target.path}`,
122
- ];
123
-
124
- if (metadata_row.length > 0) {
125
- lines.push(` ${metadata_row}`);
126
- }
127
-
128
- lines.push('', ` ${output_item.target.title}`);
129
-
130
- return lines.join('\n');
131
- }
132
-
133
- /**
134
- * @param {OutputNodeItem} output_item
135
- * @returns {string}
136
- */
137
- function formatMetadataRow(output_item) {
138
- /** @type {string[]} */
139
- const metadata_fields = [];
140
-
141
- if (isDocumentNode(output_item)) {
142
- metadata_fields.push(`kind: ${output_item.node_kind}`);
143
- } else {
144
- metadata_fields.push(`path: ${output_item.path}`);
145
- }
146
-
147
- if (output_item.status) {
148
- metadata_fields.push(`status: ${output_item.status}`);
149
- }
150
-
151
- return metadata_fields.join(' ');
152
- }
153
-
154
- /**
155
- * @param {OutputNodeItem} output_item
156
- * @returns {string}
157
- */
158
- function formatNodeHeader(output_item) {
159
- if (isDocumentNode(output_item)) {
160
- return `document ${output_item.path}`;
161
- }
162
-
163
- return `${output_item.node_kind} ${output_item.id}`;
164
- }
165
-
166
- /**
167
- * @param {OutputNodeItem} output_item
168
- * @returns {boolean}
169
- */
170
- function isDocumentNode(output_item) {
171
- return output_item.id === `doc:${output_item.path}`;
172
- }
173
-
174
- /**
175
- * @param {{ kind?: string, status?: string }} target
176
- * @returns {string}
177
- */
178
- function formatResolvedLinkMetadataRow(target) {
179
- /** @type {string[]} */
180
- const metadata_fields = [];
181
-
182
- if (target.kind) {
183
- metadata_fields.push(`kind: ${target.kind}`);
184
- }
185
-
186
- if (target.status) {
187
- metadata_fields.push(`status: ${target.status}`);
188
- }
189
-
190
- return metadata_fields.join(' ');
130
+ return formatOutputItemBlock({
131
+ header: `[${output_item.reference}] ${output_item.target.kind} ${output_item.target.path ?? output_item.target.id}`,
132
+ metadata_rows: formatResolvedLinkMetadataRows(output_item.target),
133
+ metadata_indent: ' ',
134
+ title: output_item.target.title,
135
+ });
191
136
  }
192
137
 
193
138
  /**
@@ -5,6 +5,12 @@
5
5
 
6
6
  import { Ansis } from 'ansis';
7
7
 
8
+ import {
9
+ formatOutputNodeMetadataRows,
10
+ formatResolvedLinkMetadataRows,
11
+ } from './format-output-metadata.js';
12
+ import { formatNodeHeader } from './format-node-header.js';
13
+ import { formatOutputItemBlock } from './format-output-item-block.js';
8
14
  import { layoutStoredQueries } from './layout-stored-queries.js';
9
15
  import { renderRichSource } from './render-rich-source.js';
10
16
 
@@ -96,12 +102,26 @@ async function renderRichShowOutput(output_view, render_options, ansi) {
96
102
  const rendered_source = trimTrailingLineBreaks(
97
103
  await renderRichSource(output_view, render_options),
98
104
  );
105
+ const document_summary = output_view.document
106
+ ? formatRichNodeItem(output_view.document, ansi)
107
+ : '';
99
108
 
100
- if (output_view.items.length === 0) {
109
+ if (document_summary.length === 0 && output_view.items.length === 0) {
101
110
  return `${rendered_source}\n`;
102
111
  }
103
112
 
104
- return `${rendered_source}\n\n${ansi.gray(FULL_WIDTH_DIVIDER)}\n\n${output_view.items.map((item) => formatRichResolvedLinkItem(item, ansi)).join('\n\n')}\n`;
113
+ /** @type {string[]} */
114
+ const summary_items = [];
115
+
116
+ if (document_summary.length > 0) {
117
+ summary_items.push(document_summary);
118
+ }
119
+
120
+ summary_items.push(
121
+ ...output_view.items.map((item) => formatRichResolvedLinkItem(item, ansi)),
122
+ );
123
+
124
+ return `${rendered_source}\n\n${ansi.gray(FULL_WIDTH_DIVIDER)}\n\n${summary_items.join('\n\n')}\n`;
105
125
  }
106
126
 
107
127
  /**
@@ -110,17 +130,11 @@ async function renderRichShowOutput(output_view, render_options, ansi) {
110
130
  * @returns {string}
111
131
  */
112
132
  function formatRichNodeItem(output_item, ansi) {
113
- const metadata_row = formatRichMetadataRow(output_item);
114
- /** @type {string[]} */
115
- const lines = [ansi.green(formatNodeHeader(output_item))];
116
-
117
- if (metadata_row.length > 0) {
118
- lines.push(metadata_row);
119
- }
120
-
121
- lines.push('', ` ${output_item.title}`);
122
-
123
- return lines.join('\n');
133
+ return formatOutputItemBlock({
134
+ header: ansi.green(formatNodeHeader(output_item)),
135
+ metadata_rows: formatOutputNodeMetadataRows(output_item),
136
+ title: output_item.title,
137
+ });
124
138
  }
125
139
 
126
140
  /**
@@ -140,59 +154,12 @@ function formatRichStoredQueryLine(line_segments, ansi) {
140
154
  * @returns {string}
141
155
  */
142
156
  function formatRichResolvedLinkItem(output_item, ansi) {
143
- const metadata_row = formatRichResolvedLinkMetadataRow(output_item.target);
144
- /** @type {string[]} */
145
- const lines = [
146
- `${ansi.gray(`[${output_item.reference}]`)} ${ansi.green(`document ${output_item.target.path}`)}`,
147
- ];
148
-
149
- if (metadata_row.length > 0) {
150
- lines.push(` ${metadata_row}`);
151
- }
152
-
153
- lines.push('', ` ${output_item.target.title}`);
154
-
155
- return lines.join('\n');
156
- }
157
-
158
- /**
159
- * @param {OutputNodeItem} output_item
160
- * @returns {string}
161
- */
162
- function formatRichMetadataRow(output_item) {
163
- /** @type {string[]} */
164
- const metadata_fields = [];
165
-
166
- if (isDocumentNode(output_item)) {
167
- metadata_fields.push(`kind: ${output_item.node_kind}`);
168
- } else {
169
- metadata_fields.push(`path: ${output_item.path}`);
170
- }
171
-
172
- if (output_item.status) {
173
- metadata_fields.push(`status: ${output_item.status}`);
174
- }
175
-
176
- return metadata_fields.join(' ');
177
- }
178
-
179
- /**
180
- * @param {{ kind?: string, status?: string }} target
181
- * @returns {string}
182
- */
183
- function formatRichResolvedLinkMetadataRow(target) {
184
- /** @type {string[]} */
185
- const metadata_fields = [];
186
-
187
- if (target.kind) {
188
- metadata_fields.push(`kind: ${target.kind}`);
189
- }
190
-
191
- if (target.status) {
192
- metadata_fields.push(`status: ${target.status}`);
193
- }
194
-
195
- return metadata_fields.join(' ');
157
+ return formatOutputItemBlock({
158
+ header: `${ansi.gray(`[${output_item.reference}]`)} ${ansi.green(`${output_item.target.kind} ${output_item.target.path ?? output_item.target.id}`)}`,
159
+ metadata_rows: formatResolvedLinkMetadataRows(output_item.target),
160
+ metadata_indent: ' ',
161
+ title: output_item.target.title,
162
+ });
196
163
  }
197
164
 
198
165
  /**
@@ -218,32 +185,12 @@ function styleStoredQuerySegment(line_segment, ansi) {
218
185
  }
219
186
 
220
187
  if (line_segment.kind === 'keyword') {
221
- return ansi.yellow(line_segment.text);
188
+ return ansi.gray(line_segment.text);
222
189
  }
223
190
 
224
191
  return line_segment.text;
225
192
  }
226
193
 
227
- /**
228
- * @param {OutputNodeItem} output_item
229
- * @returns {string}
230
- */
231
- function formatNodeHeader(output_item) {
232
- if (isDocumentNode(output_item)) {
233
- return `document ${output_item.path}`;
234
- }
235
-
236
- return `${output_item.node_kind} ${output_item.id}`;
237
- }
238
-
239
- /**
240
- * @param {OutputNodeItem} output_item
241
- * @returns {boolean}
242
- */
243
- function isDocumentNode(output_item) {
244
- return output_item.id === `doc:${output_item.path}`;
245
- }
246
-
247
194
  /**
248
195
  * @param {string} value
249
196
  * @returns {string}