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.
- package/bin/patram.js +25 -147
- package/lib/build-graph-identity.js +270 -0
- package/lib/build-graph.js +156 -77
- package/lib/check-graph.js +23 -7
- package/lib/claim-helpers.js +55 -0
- package/lib/cli-help-metadata.js +552 -0
- package/lib/command-output.js +83 -0
- package/lib/derived-summary.js +278 -0
- package/lib/format-derived-summary-row.js +9 -0
- package/lib/format-node-header.js +19 -0
- package/lib/format-output-item-block.js +22 -0
- package/lib/format-output-metadata.js +62 -0
- package/lib/layout-stored-queries.js +361 -0
- package/lib/list-queries.js +18 -0
- package/lib/list-source-files.js +50 -15
- package/lib/load-patram-config.js +505 -18
- package/lib/load-patram-config.types.ts +40 -0
- package/lib/load-project-graph.js +124 -0
- package/lib/output-view.types.ts +88 -0
- package/lib/parse-claims.js +38 -158
- package/lib/parse-claims.types.ts +7 -0
- package/lib/parse-cli-arguments-helpers.js +446 -0
- package/lib/parse-cli-arguments.js +266 -0
- package/lib/parse-cli-arguments.types.ts +69 -0
- package/lib/parse-cli-color-options.js +44 -0
- package/lib/parse-cli-query-pagination.js +49 -0
- package/lib/parse-jsdoc-blocks.js +184 -0
- package/lib/parse-jsdoc-claims.js +280 -0
- package/lib/parse-jsdoc-prose.js +111 -0
- package/lib/parse-markdown-claims.js +242 -0
- package/lib/parse-markdown-directives.js +136 -0
- package/lib/parse-where-clause.js +707 -0
- package/lib/parse-where-clause.types.ts +70 -0
- package/lib/patram-cli.js +464 -0
- package/lib/patram-config.js +3 -1
- package/lib/patram-config.types.ts +2 -1
- package/lib/patram.js +6 -0
- package/lib/query-graph.js +368 -0
- package/lib/query-inspection.js +523 -0
- package/lib/render-check-output.js +315 -0
- package/lib/render-cli-help.js +419 -0
- package/lib/render-json-output.js +161 -0
- package/lib/render-output-view.js +222 -0
- package/lib/render-plain-output.js +182 -0
- package/lib/render-rich-output.js +240 -0
- package/lib/render-rich-source.js +1333 -0
- package/lib/resolve-check-target.js +190 -0
- package/lib/resolve-output-mode.js +60 -0
- package/lib/resolve-patram-graph-config.js +88 -0
- package/lib/resolve-where-clause.js +66 -0
- package/lib/show-document.js +311 -0
- package/lib/source-file-defaults.js +28 -0
- package/lib/tagged-fenced-block-error.js +17 -0
- package/lib/tagged-fenced-block-markdown.js +111 -0
- package/lib/tagged-fenced-block-metadata.js +97 -0
- package/lib/tagged-fenced-block-parser.js +292 -0
- package/lib/tagged-fenced-blocks.js +100 -0
- package/lib/tagged-fenced-blocks.types.ts +38 -0
- package/lib/write-paged-output.js +87 -0
- package/package.json +28 -12
- package/bin/patram.test.js +0 -184
- package/lib/build-graph.test.js +0 -141
- package/lib/check-graph.test.js +0 -103
- package/lib/list-source-files.test.js +0 -101
- package/lib/load-patram-config.test.js +0 -211
- package/lib/parse-claims.test.js +0 -113
- 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
|
+
}
|