patram 0.0.2 → 0.2.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 (67) hide show
  1. package/bin/patram.js +25 -147
  2. package/lib/build-graph-identity.js +270 -0
  3. package/lib/build-graph.js +156 -77
  4. package/lib/check-graph.js +23 -7
  5. package/lib/claim-helpers.js +55 -0
  6. package/lib/cli-help-metadata.js +552 -0
  7. package/lib/command-output.js +83 -0
  8. package/lib/derived-summary.js +278 -0
  9. package/lib/format-derived-summary-row.js +9 -0
  10. package/lib/format-node-header.js +19 -0
  11. package/lib/format-output-item-block.js +22 -0
  12. package/lib/format-output-metadata.js +62 -0
  13. package/lib/layout-stored-queries.js +361 -0
  14. package/lib/list-queries.js +18 -0
  15. package/lib/list-source-files.js +50 -15
  16. package/lib/load-patram-config.js +505 -18
  17. package/lib/load-patram-config.types.ts +40 -0
  18. package/lib/load-project-graph.js +124 -0
  19. package/lib/output-view.types.ts +88 -0
  20. package/lib/parse-claims.js +38 -158
  21. package/lib/parse-claims.types.ts +7 -0
  22. package/lib/parse-cli-arguments-helpers.js +446 -0
  23. package/lib/parse-cli-arguments.js +266 -0
  24. package/lib/parse-cli-arguments.types.ts +69 -0
  25. package/lib/parse-cli-color-options.js +44 -0
  26. package/lib/parse-cli-query-pagination.js +49 -0
  27. package/lib/parse-jsdoc-blocks.js +184 -0
  28. package/lib/parse-jsdoc-claims.js +280 -0
  29. package/lib/parse-jsdoc-prose.js +111 -0
  30. package/lib/parse-markdown-claims.js +242 -0
  31. package/lib/parse-markdown-directives.js +136 -0
  32. package/lib/parse-where-clause.js +707 -0
  33. package/lib/parse-where-clause.types.ts +70 -0
  34. package/lib/patram-cli.js +464 -0
  35. package/lib/patram-config.js +3 -1
  36. package/lib/patram-config.types.ts +2 -1
  37. package/lib/patram.js +6 -0
  38. package/lib/query-graph.js +368 -0
  39. package/lib/query-inspection.js +523 -0
  40. package/lib/render-check-output.js +315 -0
  41. package/lib/render-cli-help.js +419 -0
  42. package/lib/render-json-output.js +161 -0
  43. package/lib/render-output-view.js +222 -0
  44. package/lib/render-plain-output.js +182 -0
  45. package/lib/render-rich-output.js +240 -0
  46. package/lib/render-rich-source.js +1333 -0
  47. package/lib/resolve-check-target.js +190 -0
  48. package/lib/resolve-output-mode.js +60 -0
  49. package/lib/resolve-patram-graph-config.js +88 -0
  50. package/lib/resolve-where-clause.js +66 -0
  51. package/lib/show-document.js +311 -0
  52. package/lib/source-file-defaults.js +28 -0
  53. package/lib/tagged-fenced-block-error.js +17 -0
  54. package/lib/tagged-fenced-block-markdown.js +111 -0
  55. package/lib/tagged-fenced-block-metadata.js +97 -0
  56. package/lib/tagged-fenced-block-parser.js +292 -0
  57. package/lib/tagged-fenced-blocks.js +100 -0
  58. package/lib/tagged-fenced-blocks.types.ts +38 -0
  59. package/lib/write-paged-output.js +87 -0
  60. package/package.json +28 -12
  61. package/bin/patram.test.js +0 -184
  62. package/lib/build-graph.test.js +0 -141
  63. package/lib/check-graph.test.js +0 -103
  64. package/lib/list-source-files.test.js +0 -101
  65. package/lib/load-patram-config.test.js +0 -211
  66. package/lib/parse-claims.test.js +0 -113
  67. package/lib/patram-config.test.js +0 -147
@@ -0,0 +1,222 @@
1
+ /** @import * as $k$$l$output$j$view$k$types$k$ts from './output-view.types.ts'; */
2
+ /**
3
+ * @import { BuildGraphResult, GraphNode } from './build-graph.types.ts';
4
+ * @import { DerivedSummaryEvaluator } from './derived-summary.js';
5
+ * @import { ParsedCliArguments } from './parse-cli-arguments.types.ts';
6
+ * @import { OutputStoredQueryItem, OutputView, ResolvedOutputMode, ShowOutputView } from './output-view.types.ts';
7
+ */
8
+
9
+ import { renderJsonOutput } from './render-json-output.js';
10
+ import { renderPlainOutput } from './render-plain-output.js';
11
+ import { renderRichOutput } from './render-rich-output.js';
12
+
13
+ /**
14
+ * Shared command output views.
15
+ *
16
+ * Normalizes `query`, `queries`, and `show` results into renderer-specific
17
+ * output models.
18
+ *
19
+ * Kind: output
20
+ * Status: active
21
+ * Tracked in: ../docs/plans/v0/source-anchor-dogfooding.md
22
+ * Decided by: ../docs/decisions/cli-output-architecture.md
23
+ * @patram
24
+ * @see {@link ./show-document.js}
25
+ * @see {@link ../docs/decisions/cli-output-architecture.md}
26
+ */
27
+
28
+ /**
29
+ * Create a shared output view from one command result.
30
+ *
31
+ * @param {'query' | 'queries'} command_name
32
+ * @param {GraphNode[] | { name: string, where: string }[]} command_items
33
+ * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, hints?: string[], limit?: number, offset?: number, total_count?: number }=} command_options
34
+ * @returns {OutputView}
35
+ */
36
+ export function createOutputView(command_name, command_items, command_options) {
37
+ if (command_name === 'query') {
38
+ return createQueryOutputView(
39
+ /** @type {GraphNode[]} */ (command_items),
40
+ command_options,
41
+ );
42
+ }
43
+
44
+ if (command_name === 'queries') {
45
+ return createStoredQueriesOutputView(
46
+ /** @type {OutputStoredQueryItem[]} */ (command_items),
47
+ );
48
+ }
49
+
50
+ throw new Error(`Unsupported output view command "${command_name}".`);
51
+ }
52
+
53
+ /**
54
+ * Create a shared output view for the show command.
55
+ *
56
+ * @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
57
+ * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, graph_nodes?: BuildGraphResult['nodes'] }=} command_options
58
+ * @returns {ShowOutputView}
59
+ */
60
+ export function createShowOutputView(show_output, command_options = {}) {
61
+ const shown_document_node =
62
+ command_options.graph_nodes?.[`doc:${show_output.path}`];
63
+
64
+ return {
65
+ command: 'show',
66
+ document: shown_document_node
67
+ ? createOutputNodeItem(
68
+ shown_document_node,
69
+ command_options.derived_summary_evaluator?.evaluate(
70
+ shown_document_node,
71
+ ) ?? null,
72
+ )
73
+ : undefined,
74
+ hints: [],
75
+ items: show_output.resolved_links.map((resolved_link) => ({
76
+ kind: 'resolved_link',
77
+ label: resolved_link.label,
78
+ reference: resolved_link.reference,
79
+ target: createResolvedLinkTarget(
80
+ resolved_link.target,
81
+ command_options.graph_nodes?.[`doc:${resolved_link.target.path}`]
82
+ ? (command_options.derived_summary_evaluator?.evaluate(
83
+ command_options.graph_nodes[`doc:${resolved_link.target.path}`],
84
+ ) ?? null)
85
+ : null,
86
+ ),
87
+ })),
88
+ path: show_output.path,
89
+ rendered_source: show_output.rendered_source,
90
+ source: show_output.source,
91
+ summary: {
92
+ count: show_output.resolved_links.length,
93
+ kind: 'resolved_link_list',
94
+ },
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Render one shared output view through the resolved renderer.
100
+ *
101
+ * @param {OutputView} output_view
102
+ * @param {ResolvedOutputMode} output_mode
103
+ * @param {ParsedCliArguments} parsed_arguments
104
+ * @returns {Promise<string>}
105
+ */
106
+ export async function renderOutputView(
107
+ output_view,
108
+ output_mode,
109
+ parsed_arguments,
110
+ ) {
111
+ if (output_mode.renderer_name === 'json') {
112
+ return renderJsonOutput(output_view);
113
+ }
114
+
115
+ if (output_mode.renderer_name === 'plain') {
116
+ return renderPlainOutput(output_view);
117
+ }
118
+
119
+ return renderRichOutput(output_view, {
120
+ color_enabled: output_mode.color_enabled,
121
+ color_mode: parsed_arguments.color_mode,
122
+ });
123
+ }
124
+
125
+ /**
126
+ * @param {GraphNode[]} graph_nodes
127
+ * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, hints?: string[], limit?: number, offset?: number, total_count?: number }=} command_options
128
+ * @returns {OutputView}
129
+ */
130
+ function createQueryOutputView(graph_nodes, command_options = {}) {
131
+ const total_count = command_options.total_count ?? graph_nodes.length;
132
+
133
+ return {
134
+ command: 'query',
135
+ hints:
136
+ command_options.hints ??
137
+ (total_count === 0 ? ['Try: patram query --where "kind=task"'] : []),
138
+ items: graph_nodes.map((graph_node) =>
139
+ createOutputNodeItem(
140
+ graph_node,
141
+ command_options.derived_summary_evaluator?.evaluate(graph_node) ?? null,
142
+ ),
143
+ ),
144
+ summary: {
145
+ count: graph_nodes.length,
146
+ kind: 'result_list',
147
+ limit: command_options.limit ?? graph_nodes.length,
148
+ offset: command_options.offset ?? 0,
149
+ total_count,
150
+ },
151
+ };
152
+ }
153
+
154
+ /**
155
+ * @param {{ name: string, where: string }[]} stored_queries
156
+ * @returns {OutputView}
157
+ */
158
+ function createStoredQueriesOutputView(stored_queries) {
159
+ return {
160
+ command: 'queries',
161
+ hints: [],
162
+ items: stored_queries.map((stored_query) => ({
163
+ kind: 'stored_query',
164
+ name: stored_query.name,
165
+ where: stored_query.where,
166
+ })),
167
+ summary: {
168
+ count: stored_queries.length,
169
+ kind: 'stored_query_list',
170
+ },
171
+ };
172
+ }
173
+
174
+ /**
175
+ * @param {GraphNode} graph_node
176
+ * @param {import('./output-view.types.ts').OutputDerivedSummary | null} derived_summary
177
+ * @returns {$k$$l$output$j$view$k$types$k$ts.OutputNodeItem}
178
+ */
179
+ function createOutputNodeItem(graph_node, derived_summary) {
180
+ const title =
181
+ graph_node.title ?? graph_node.label ?? graph_node.path ?? graph_node.key;
182
+
183
+ if (!title || !graph_node.path) {
184
+ throw new Error(
185
+ `Expected graph node "${graph_node.id}" to have a title and path.`,
186
+ );
187
+ }
188
+
189
+ return {
190
+ derived_summary: derived_summary ?? undefined,
191
+ id: graph_node.id,
192
+ kind: 'node',
193
+ node_kind: graph_node.kind,
194
+ path: graph_node.path,
195
+ status: graph_node.status,
196
+ title,
197
+ };
198
+ }
199
+
200
+ /**
201
+ * @param {{ kind?: string, path: string, status?: string, title: string }} target
202
+ * @param {import('./output-view.types.ts').OutputDerivedSummary | null} derived_summary
203
+ * @returns {$k$$l$output$j$view$k$types$k$ts.OutputResolvedLinkTarget}
204
+ */
205
+ function createResolvedLinkTarget(target, derived_summary) {
206
+ /** @type {$k$$l$output$j$view$k$types$k$ts.OutputResolvedLinkTarget} */
207
+ const resolved_target = {
208
+ derived_summary: derived_summary ?? undefined,
209
+ path: target.path,
210
+ title: target.title,
211
+ };
212
+
213
+ if (target.kind && target.kind !== 'document') {
214
+ resolved_target.kind = target.kind;
215
+ }
216
+
217
+ if (target.status) {
218
+ resolved_target.status = target.status;
219
+ }
220
+
221
+ return resolved_target;
222
+ }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * @import { OutputNodeItem, OutputResolvedLinkItem, OutputStoredQueryItem, OutputView, QueryOutputView, ShowOutputView } from './output-view.types.ts';
3
+ */
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';
11
+ import { layoutStoredQueries } from './layout-stored-queries.js';
12
+
13
+ /**
14
+ * Render the canonical plain output for one output view.
15
+ *
16
+ * @param {OutputView} output_view
17
+ * @returns {string}
18
+ */
19
+ export function renderPlainOutput(output_view) {
20
+ if (output_view.command === 'query') {
21
+ return renderPlainQueryOutput(output_view);
22
+ }
23
+
24
+ if (output_view.command === 'queries') {
25
+ return renderPlainStoredQueries(output_view.items);
26
+ }
27
+
28
+ if (output_view.command === 'show') {
29
+ return renderPlainShowOutput(output_view);
30
+ }
31
+
32
+ throw new Error('Unsupported output view command.');
33
+ }
34
+
35
+ /**
36
+ * @param {QueryOutputView} output_view
37
+ * @returns {string}
38
+ */
39
+ function renderPlainQueryOutput(output_view) {
40
+ const footer = renderPlainQueryFooter(output_view.summary, output_view.hints);
41
+
42
+ if (output_view.items.length === 0) {
43
+ return renderPlainEmptyQuery(footer);
44
+ }
45
+
46
+ if (footer.length === 0) {
47
+ return `${output_view.items.map(formatPlainNodeItem).join('\n\n')}\n`;
48
+ }
49
+
50
+ return `${output_view.items.map(formatPlainNodeItem).join('\n\n')}\n\n${footer}\n`;
51
+ }
52
+
53
+ /**
54
+ * @param {string} footer
55
+ * @returns {string}
56
+ */
57
+ function renderPlainEmptyQuery(footer) {
58
+ if (footer.length === 0) {
59
+ return 'No matches.\n';
60
+ }
61
+
62
+ return `No matches.\n${footer}\n`;
63
+ }
64
+
65
+ /**
66
+ * @param {OutputStoredQueryItem[]} output_items
67
+ * @returns {string}
68
+ */
69
+ function renderPlainStoredQueries(output_items) {
70
+ if (output_items.length === 0) {
71
+ return '';
72
+ }
73
+
74
+ return `${layoutStoredQueries(output_items)
75
+ .map(formatPlainStoredQueryLine)
76
+ .join('\n')}\n`;
77
+ }
78
+
79
+ /**
80
+ * @param {ShowOutputView} output_view
81
+ * @returns {string}
82
+ */
83
+ function renderPlainShowOutput(output_view) {
84
+ const rendered_source = trimTrailingLineBreaks(output_view.rendered_source);
85
+ const document_summary = output_view.document
86
+ ? formatPlainNodeItem(output_view.document)
87
+ : '';
88
+
89
+ if (document_summary.length === 0 && output_view.items.length === 0) {
90
+ return `${rendered_source}\n`;
91
+ }
92
+
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`;
103
+ }
104
+
105
+ /**
106
+ * @param {OutputNodeItem} output_item
107
+ * @returns {string}
108
+ */
109
+ function formatPlainNodeItem(output_item) {
110
+ return formatOutputItemBlock({
111
+ header: formatNodeHeader(output_item),
112
+ metadata_rows: formatOutputNodeMetadataRows(output_item),
113
+ title: output_item.title,
114
+ });
115
+ }
116
+
117
+ /**
118
+ * @param {{ text: string }[]} line_segments
119
+ * @returns {string}
120
+ */
121
+ function formatPlainStoredQueryLine(line_segments) {
122
+ return line_segments.map((segment) => segment.text).join('');
123
+ }
124
+
125
+ /**
126
+ * @param {OutputResolvedLinkItem} output_item
127
+ * @returns {string}
128
+ */
129
+ function formatPlainResolvedLinkItem(output_item) {
130
+ return formatOutputItemBlock({
131
+ header: `[${output_item.reference}] document ${output_item.target.path}`,
132
+ metadata_rows: formatResolvedLinkMetadataRows(output_item.target),
133
+ metadata_indent: ' ',
134
+ title: output_item.target.title,
135
+ });
136
+ }
137
+
138
+ /**
139
+ * @param {string} value
140
+ * @returns {string}
141
+ */
142
+ function trimTrailingLineBreaks(value) {
143
+ return value.replace(/\n+$/du, '');
144
+ }
145
+
146
+ /**
147
+ * @param {{ count: number, limit: number, offset: number, total_count: number }} summary
148
+ * @returns {string}
149
+ */
150
+ function formatQuerySummary(summary) {
151
+ if (!shouldRenderQuerySummary(summary)) {
152
+ return '';
153
+ }
154
+
155
+ return `Showing ${summary.count} of ${summary.total_count} matches.`;
156
+ }
157
+
158
+ /**
159
+ * @param {{ count: number, limit: number, offset: number, total_count: number }} summary
160
+ * @param {string[]} hints
161
+ * @returns {string}
162
+ */
163
+ function renderPlainQueryFooter(summary, hints) {
164
+ const summary_line = formatQuerySummary(summary);
165
+ const footer_lines = [];
166
+
167
+ if (summary_line.length > 0) {
168
+ footer_lines.push(summary_line);
169
+ }
170
+
171
+ footer_lines.push(...hints);
172
+
173
+ return footer_lines.join('\n');
174
+ }
175
+
176
+ /**
177
+ * @param {{ limit: number, offset: number, total_count: number }} summary
178
+ * @returns {boolean}
179
+ */
180
+ function shouldRenderQuerySummary(summary) {
181
+ return summary.offset > 0 || summary.total_count > summary.limit;
182
+ }
@@ -0,0 +1,240 @@
1
+ /**
2
+ * @import { CliColorMode } from './parse-cli-arguments.types.ts';
3
+ * @import { OutputNodeItem, OutputResolvedLinkItem, OutputStoredQueryItem, OutputView, QueryOutputView, ShowOutputView } from './output-view.types.ts';
4
+ */
5
+
6
+ import { Ansis } from 'ansis';
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';
14
+ import { layoutStoredQueries } from './layout-stored-queries.js';
15
+ import { renderRichSource } from './render-rich-source.js';
16
+
17
+ const FULL_WIDTH_DIVIDER = ` ${'─'.repeat(78)} `;
18
+
19
+ /**
20
+ * Render styled rich output while preserving the plain layout.
21
+ *
22
+ * @param {OutputView} output_view
23
+ * @param {{ color_mode: CliColorMode, color_enabled: boolean }} render_options
24
+ * @returns {Promise<string>}
25
+ */
26
+ export async function renderRichOutput(output_view, render_options) {
27
+ const ansi = createAnsi(render_options.color_enabled);
28
+
29
+ if (output_view.command === 'query') {
30
+ return renderRichQueryOutput(output_view, ansi);
31
+ }
32
+
33
+ if (output_view.command === 'queries') {
34
+ return renderRichStoredQueries(output_view.items, ansi);
35
+ }
36
+
37
+ if (output_view.command === 'show') {
38
+ return renderRichShowOutput(output_view, render_options, ansi);
39
+ }
40
+
41
+ throw new Error('Unsupported output view command.');
42
+ }
43
+
44
+ /**
45
+ * @param {QueryOutputView} output_view
46
+ * @param {Ansis} ansi
47
+ * @returns {string}
48
+ */
49
+ function renderRichQueryOutput(output_view, ansi) {
50
+ const footer = renderRichQueryFooter(
51
+ output_view.summary,
52
+ output_view.hints,
53
+ ansi,
54
+ );
55
+
56
+ if (output_view.items.length === 0) {
57
+ return renderRichEmptyQuery(footer, ansi);
58
+ }
59
+
60
+ if (footer.length === 0) {
61
+ return `${output_view.items.map((item) => formatRichNodeItem(item, ansi)).join('\n\n')}\n`;
62
+ }
63
+
64
+ return `${output_view.items.map((item) => formatRichNodeItem(item, ansi)).join('\n\n')}\n\n${footer}\n`;
65
+ }
66
+
67
+ /**
68
+ * @param {string} footer
69
+ * @param {Ansis} ansi
70
+ * @returns {string}
71
+ */
72
+ function renderRichEmptyQuery(footer, ansi) {
73
+ if (footer.length === 0) {
74
+ return `${ansi.yellow('No matches.')}\n`;
75
+ }
76
+
77
+ return `${ansi.yellow('No matches.')}\n${footer}\n`;
78
+ }
79
+
80
+ /**
81
+ * @param {OutputStoredQueryItem[]} output_items
82
+ * @param {Ansis} ansi
83
+ * @returns {string}
84
+ */
85
+ function renderRichStoredQueries(output_items, ansi) {
86
+ if (output_items.length === 0) {
87
+ return '';
88
+ }
89
+
90
+ return `${layoutStoredQueries(output_items)
91
+ .map((line_segments) => formatRichStoredQueryLine(line_segments, ansi))
92
+ .join('\n')}\n`;
93
+ }
94
+
95
+ /**
96
+ * @param {ShowOutputView} output_view
97
+ * @param {{ color_mode: CliColorMode, color_enabled: boolean }} render_options
98
+ * @param {Ansis} ansi
99
+ * @returns {Promise<string>}
100
+ */
101
+ async function renderRichShowOutput(output_view, render_options, ansi) {
102
+ const rendered_source = trimTrailingLineBreaks(
103
+ await renderRichSource(output_view, render_options),
104
+ );
105
+ const document_summary = output_view.document
106
+ ? formatRichNodeItem(output_view.document, ansi)
107
+ : '';
108
+
109
+ if (document_summary.length === 0 && output_view.items.length === 0) {
110
+ return `${rendered_source}\n`;
111
+ }
112
+
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`;
125
+ }
126
+
127
+ /**
128
+ * @param {OutputNodeItem} output_item
129
+ * @param {Ansis} ansi
130
+ * @returns {string}
131
+ */
132
+ function formatRichNodeItem(output_item, ansi) {
133
+ return formatOutputItemBlock({
134
+ header: ansi.green(formatNodeHeader(output_item)),
135
+ metadata_rows: formatOutputNodeMetadataRows(output_item),
136
+ title: output_item.title,
137
+ });
138
+ }
139
+
140
+ /**
141
+ * @param {{ kind: 'field_name' | 'keyword' | 'literal' | 'name' | 'operator' | 'plain', text: string }[]} line_segments
142
+ * @param {Ansis} ansi
143
+ * @returns {string}
144
+ */
145
+ function formatRichStoredQueryLine(line_segments, ansi) {
146
+ return line_segments
147
+ .map((line_segment) => styleStoredQuerySegment(line_segment, ansi))
148
+ .join('');
149
+ }
150
+
151
+ /**
152
+ * @param {OutputResolvedLinkItem} output_item
153
+ * @param {Ansis} ansi
154
+ * @returns {string}
155
+ */
156
+ function formatRichResolvedLinkItem(output_item, ansi) {
157
+ return formatOutputItemBlock({
158
+ header: `${ansi.gray(`[${output_item.reference}]`)} ${ansi.green(`document ${output_item.target.path}`)}`,
159
+ metadata_rows: formatResolvedLinkMetadataRows(output_item.target),
160
+ metadata_indent: ' ',
161
+ title: output_item.target.title,
162
+ });
163
+ }
164
+
165
+ /**
166
+ * @param {boolean} color_enabled
167
+ * @returns {Ansis}
168
+ */
169
+ function createAnsi(color_enabled) {
170
+ return new Ansis(color_enabled ? 3 : 0);
171
+ }
172
+
173
+ /**
174
+ * @param {{ kind: 'field_name' | 'keyword' | 'literal' | 'name' | 'operator' | 'plain', text: string }} line_segment
175
+ * @param {Ansis} ansi
176
+ * @returns {string}
177
+ */
178
+ function styleStoredQuerySegment(line_segment, ansi) {
179
+ if (line_segment.kind === 'name') {
180
+ return ansi.green(line_segment.text);
181
+ }
182
+
183
+ if (line_segment.kind === 'operator') {
184
+ return ansi.gray(line_segment.text);
185
+ }
186
+
187
+ if (line_segment.kind === 'keyword') {
188
+ return ansi.gray(line_segment.text);
189
+ }
190
+
191
+ return line_segment.text;
192
+ }
193
+
194
+ /**
195
+ * @param {string} value
196
+ * @returns {string}
197
+ */
198
+ function trimTrailingLineBreaks(value) {
199
+ return value.replace(/\n+$/du, '');
200
+ }
201
+
202
+ /**
203
+ * @param {{ count: number, limit: number, offset: number, total_count: number }} summary
204
+ * @returns {string}
205
+ */
206
+ function formatQuerySummary(summary) {
207
+ if (!shouldRenderQuerySummary(summary)) {
208
+ return '';
209
+ }
210
+
211
+ return `Showing ${summary.count} of ${summary.total_count} matches.`;
212
+ }
213
+
214
+ /**
215
+ * @param {{ count: number, limit: number, offset: number, total_count: number }} summary
216
+ * @param {string[]} hints
217
+ * @param {Ansis} ansi
218
+ * @returns {string}
219
+ */
220
+ function renderRichQueryFooter(summary, hints, ansi) {
221
+ const summary_line = formatQuerySummary(summary);
222
+ /** @type {string[]} */
223
+ const footer_lines = [];
224
+
225
+ if (summary_line.length > 0) {
226
+ footer_lines.push(summary_line);
227
+ }
228
+
229
+ footer_lines.push(...hints.map((hint) => ansi.gray(hint)));
230
+
231
+ return footer_lines.join('\n');
232
+ }
233
+
234
+ /**
235
+ * @param {{ limit: number, offset: number, total_count: number }} summary
236
+ * @returns {boolean}
237
+ */
238
+ function shouldRenderQuerySummary(summary) {
239
+ return summary.offset > 0 || summary.total_count > summary.limit;
240
+ }