patram 0.0.2 → 0.1.1

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 (51) hide show
  1. package/bin/patram.js +25 -147
  2. package/lib/build-graph-identity.js +238 -0
  3. package/lib/build-graph.js +143 -77
  4. package/lib/check-graph.js +23 -7
  5. package/lib/claim-helpers.js +55 -0
  6. package/lib/command-output.js +83 -0
  7. package/lib/layout-stored-queries.js +213 -0
  8. package/lib/list-queries.js +18 -0
  9. package/lib/list-source-files.js +50 -15
  10. package/lib/load-patram-config.js +106 -18
  11. package/lib/load-patram-config.types.ts +9 -0
  12. package/lib/load-project-graph.js +124 -0
  13. package/lib/output-view.types.ts +73 -0
  14. package/lib/parse-claims.js +38 -158
  15. package/lib/parse-claims.types.ts +7 -0
  16. package/lib/parse-cli-arguments-helpers.js +273 -0
  17. package/lib/parse-cli-arguments.js +114 -0
  18. package/lib/parse-cli-arguments.types.ts +24 -0
  19. package/lib/parse-cli-color-options.js +44 -0
  20. package/lib/parse-cli-query-pagination.js +49 -0
  21. package/lib/parse-jsdoc-blocks.js +184 -0
  22. package/lib/parse-jsdoc-claims.js +280 -0
  23. package/lib/parse-jsdoc-prose.js +111 -0
  24. package/lib/parse-markdown-claims.js +242 -0
  25. package/lib/parse-markdown-directives.js +136 -0
  26. package/lib/parse-where-clause.js +312 -0
  27. package/lib/patram-cli.js +337 -0
  28. package/lib/patram-config.js +3 -1
  29. package/lib/patram-config.types.ts +2 -1
  30. package/lib/query-graph.js +256 -0
  31. package/lib/render-check-output.js +315 -0
  32. package/lib/render-json-output.js +108 -0
  33. package/lib/render-output-view.js +193 -0
  34. package/lib/render-plain-output.js +237 -0
  35. package/lib/render-rich-output.js +293 -0
  36. package/lib/render-rich-source.js +1333 -0
  37. package/lib/resolve-check-target.js +190 -0
  38. package/lib/resolve-output-mode.js +60 -0
  39. package/lib/resolve-patram-graph-config.js +88 -0
  40. package/lib/resolve-where-clause.js +51 -0
  41. package/lib/show-document.js +311 -0
  42. package/lib/source-file-defaults.js +28 -0
  43. package/lib/write-paged-output.js +87 -0
  44. package/package.json +21 -10
  45. package/bin/patram.test.js +0 -184
  46. package/lib/build-graph.test.js +0 -141
  47. package/lib/check-graph.test.js +0 -103
  48. package/lib/list-source-files.test.js +0 -101
  49. package/lib/load-patram-config.test.js +0 -211
  50. package/lib/parse-claims.test.js +0 -113
  51. package/lib/patram-config.test.js +0 -147
@@ -0,0 +1,237 @@
1
+ /**
2
+ * @import { OutputNodeItem, OutputResolvedLinkItem, OutputStoredQueryItem, OutputView, QueryOutputView, ShowOutputView } from './output-view.types.ts';
3
+ */
4
+
5
+ import { layoutStoredQueries } from './layout-stored-queries.js';
6
+
7
+ /**
8
+ * Render the canonical plain output for one output view.
9
+ *
10
+ * @param {OutputView} output_view
11
+ * @returns {string}
12
+ */
13
+ export function renderPlainOutput(output_view) {
14
+ if (output_view.command === 'query') {
15
+ return renderPlainQueryOutput(output_view);
16
+ }
17
+
18
+ if (output_view.command === 'queries') {
19
+ return renderPlainStoredQueries(output_view.items);
20
+ }
21
+
22
+ if (output_view.command === 'show') {
23
+ return renderPlainShowOutput(output_view);
24
+ }
25
+
26
+ throw new Error('Unsupported output view command.');
27
+ }
28
+
29
+ /**
30
+ * @param {QueryOutputView} output_view
31
+ * @returns {string}
32
+ */
33
+ function renderPlainQueryOutput(output_view) {
34
+ const footer = renderPlainQueryFooter(output_view.summary, output_view.hints);
35
+
36
+ if (output_view.items.length === 0) {
37
+ return renderPlainEmptyQuery(footer);
38
+ }
39
+
40
+ if (footer.length === 0) {
41
+ return `${output_view.items.map(formatPlainNodeItem).join('\n\n')}\n`;
42
+ }
43
+
44
+ return `${output_view.items.map(formatPlainNodeItem).join('\n\n')}\n\n${footer}\n`;
45
+ }
46
+
47
+ /**
48
+ * @param {string} footer
49
+ * @returns {string}
50
+ */
51
+ function renderPlainEmptyQuery(footer) {
52
+ if (footer.length === 0) {
53
+ return 'No matches.\n';
54
+ }
55
+
56
+ return `No matches.\n${footer}\n`;
57
+ }
58
+
59
+ /**
60
+ * @param {OutputStoredQueryItem[]} output_items
61
+ * @returns {string}
62
+ */
63
+ function renderPlainStoredQueries(output_items) {
64
+ if (output_items.length === 0) {
65
+ return '';
66
+ }
67
+
68
+ return `${layoutStoredQueries(output_items)
69
+ .map(formatPlainStoredQueryLine)
70
+ .join('\n')}\n`;
71
+ }
72
+
73
+ /**
74
+ * @param {ShowOutputView} output_view
75
+ * @returns {string}
76
+ */
77
+ function renderPlainShowOutput(output_view) {
78
+ const rendered_source = trimTrailingLineBreaks(output_view.rendered_source);
79
+
80
+ if (output_view.items.length === 0) {
81
+ return `${rendered_source}\n`;
82
+ }
83
+
84
+ return `${rendered_source}\n\n----------------\n${output_view.items.map(formatPlainResolvedLinkItem).join('\n\n')}\n`;
85
+ }
86
+
87
+ /**
88
+ * @param {OutputNodeItem} output_item
89
+ * @returns {string}
90
+ */
91
+ 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');
103
+ }
104
+
105
+ /**
106
+ * @param {{ text: string }[]} line_segments
107
+ * @returns {string}
108
+ */
109
+ function formatPlainStoredQueryLine(line_segments) {
110
+ return line_segments.map((segment) => segment.text).join('');
111
+ }
112
+
113
+ /**
114
+ * @param {OutputResolvedLinkItem} output_item
115
+ * @returns {string}
116
+ */
117
+ 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(' ');
191
+ }
192
+
193
+ /**
194
+ * @param {string} value
195
+ * @returns {string}
196
+ */
197
+ function trimTrailingLineBreaks(value) {
198
+ return value.replace(/\n+$/du, '');
199
+ }
200
+
201
+ /**
202
+ * @param {{ count: number, limit: number, offset: number, total_count: number }} summary
203
+ * @returns {string}
204
+ */
205
+ function formatQuerySummary(summary) {
206
+ if (!shouldRenderQuerySummary(summary)) {
207
+ return '';
208
+ }
209
+
210
+ return `Showing ${summary.count} of ${summary.total_count} matches.`;
211
+ }
212
+
213
+ /**
214
+ * @param {{ count: number, limit: number, offset: number, total_count: number }} summary
215
+ * @param {string[]} hints
216
+ * @returns {string}
217
+ */
218
+ function renderPlainQueryFooter(summary, hints) {
219
+ const summary_line = formatQuerySummary(summary);
220
+ const footer_lines = [];
221
+
222
+ if (summary_line.length > 0) {
223
+ footer_lines.push(summary_line);
224
+ }
225
+
226
+ footer_lines.push(...hints);
227
+
228
+ return footer_lines.join('\n');
229
+ }
230
+
231
+ /**
232
+ * @param {{ limit: number, offset: number, total_count: number }} summary
233
+ * @returns {boolean}
234
+ */
235
+ function shouldRenderQuerySummary(summary) {
236
+ return summary.offset > 0 || summary.total_count > summary.limit;
237
+ }
@@ -0,0 +1,293 @@
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 { layoutStoredQueries } from './layout-stored-queries.js';
9
+ import { renderRichSource } from './render-rich-source.js';
10
+
11
+ const FULL_WIDTH_DIVIDER = ` ${'─'.repeat(78)} `;
12
+
13
+ /**
14
+ * Render styled rich output while preserving the plain layout.
15
+ *
16
+ * @param {OutputView} output_view
17
+ * @param {{ color_mode: CliColorMode, color_enabled: boolean }} render_options
18
+ * @returns {Promise<string>}
19
+ */
20
+ export async function renderRichOutput(output_view, render_options) {
21
+ const ansi = createAnsi(render_options.color_enabled);
22
+
23
+ if (output_view.command === 'query') {
24
+ return renderRichQueryOutput(output_view, ansi);
25
+ }
26
+
27
+ if (output_view.command === 'queries') {
28
+ return renderRichStoredQueries(output_view.items, ansi);
29
+ }
30
+
31
+ if (output_view.command === 'show') {
32
+ return renderRichShowOutput(output_view, render_options, ansi);
33
+ }
34
+
35
+ throw new Error('Unsupported output view command.');
36
+ }
37
+
38
+ /**
39
+ * @param {QueryOutputView} output_view
40
+ * @param {Ansis} ansi
41
+ * @returns {string}
42
+ */
43
+ function renderRichQueryOutput(output_view, ansi) {
44
+ const footer = renderRichQueryFooter(
45
+ output_view.summary,
46
+ output_view.hints,
47
+ ansi,
48
+ );
49
+
50
+ if (output_view.items.length === 0) {
51
+ return renderRichEmptyQuery(footer, ansi);
52
+ }
53
+
54
+ if (footer.length === 0) {
55
+ return `${output_view.items.map((item) => formatRichNodeItem(item, ansi)).join('\n\n')}\n`;
56
+ }
57
+
58
+ return `${output_view.items.map((item) => formatRichNodeItem(item, ansi)).join('\n\n')}\n\n${footer}\n`;
59
+ }
60
+
61
+ /**
62
+ * @param {string} footer
63
+ * @param {Ansis} ansi
64
+ * @returns {string}
65
+ */
66
+ function renderRichEmptyQuery(footer, ansi) {
67
+ if (footer.length === 0) {
68
+ return `${ansi.yellow('No matches.')}\n`;
69
+ }
70
+
71
+ return `${ansi.yellow('No matches.')}\n${footer}\n`;
72
+ }
73
+
74
+ /**
75
+ * @param {OutputStoredQueryItem[]} output_items
76
+ * @param {Ansis} ansi
77
+ * @returns {string}
78
+ */
79
+ function renderRichStoredQueries(output_items, ansi) {
80
+ if (output_items.length === 0) {
81
+ return '';
82
+ }
83
+
84
+ return `${layoutStoredQueries(output_items)
85
+ .map((line_segments) => formatRichStoredQueryLine(line_segments, ansi))
86
+ .join('\n')}\n`;
87
+ }
88
+
89
+ /**
90
+ * @param {ShowOutputView} output_view
91
+ * @param {{ color_mode: CliColorMode, color_enabled: boolean }} render_options
92
+ * @param {Ansis} ansi
93
+ * @returns {Promise<string>}
94
+ */
95
+ async function renderRichShowOutput(output_view, render_options, ansi) {
96
+ const rendered_source = trimTrailingLineBreaks(
97
+ await renderRichSource(output_view, render_options),
98
+ );
99
+
100
+ if (output_view.items.length === 0) {
101
+ return `${rendered_source}\n`;
102
+ }
103
+
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`;
105
+ }
106
+
107
+ /**
108
+ * @param {OutputNodeItem} output_item
109
+ * @param {Ansis} ansi
110
+ * @returns {string}
111
+ */
112
+ 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');
124
+ }
125
+
126
+ /**
127
+ * @param {{ kind: 'field_name' | 'keyword' | 'literal' | 'name' | 'operator' | 'plain', text: string }[]} line_segments
128
+ * @param {Ansis} ansi
129
+ * @returns {string}
130
+ */
131
+ function formatRichStoredQueryLine(line_segments, ansi) {
132
+ return line_segments
133
+ .map((line_segment) => styleStoredQuerySegment(line_segment, ansi))
134
+ .join('');
135
+ }
136
+
137
+ /**
138
+ * @param {OutputResolvedLinkItem} output_item
139
+ * @param {Ansis} ansi
140
+ * @returns {string}
141
+ */
142
+ 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(' ');
196
+ }
197
+
198
+ /**
199
+ * @param {boolean} color_enabled
200
+ * @returns {Ansis}
201
+ */
202
+ function createAnsi(color_enabled) {
203
+ return new Ansis(color_enabled ? 3 : 0);
204
+ }
205
+
206
+ /**
207
+ * @param {{ kind: 'field_name' | 'keyword' | 'literal' | 'name' | 'operator' | 'plain', text: string }} line_segment
208
+ * @param {Ansis} ansi
209
+ * @returns {string}
210
+ */
211
+ function styleStoredQuerySegment(line_segment, ansi) {
212
+ if (line_segment.kind === 'name') {
213
+ return ansi.green(line_segment.text);
214
+ }
215
+
216
+ if (line_segment.kind === 'operator') {
217
+ return ansi.gray(line_segment.text);
218
+ }
219
+
220
+ if (line_segment.kind === 'keyword') {
221
+ return ansi.yellow(line_segment.text);
222
+ }
223
+
224
+ return line_segment.text;
225
+ }
226
+
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
+ /**
248
+ * @param {string} value
249
+ * @returns {string}
250
+ */
251
+ function trimTrailingLineBreaks(value) {
252
+ return value.replace(/\n+$/du, '');
253
+ }
254
+
255
+ /**
256
+ * @param {{ count: number, limit: number, offset: number, total_count: number }} summary
257
+ * @returns {string}
258
+ */
259
+ function formatQuerySummary(summary) {
260
+ if (!shouldRenderQuerySummary(summary)) {
261
+ return '';
262
+ }
263
+
264
+ return `Showing ${summary.count} of ${summary.total_count} matches.`;
265
+ }
266
+
267
+ /**
268
+ * @param {{ count: number, limit: number, offset: number, total_count: number }} summary
269
+ * @param {string[]} hints
270
+ * @param {Ansis} ansi
271
+ * @returns {string}
272
+ */
273
+ function renderRichQueryFooter(summary, hints, ansi) {
274
+ const summary_line = formatQuerySummary(summary);
275
+ /** @type {string[]} */
276
+ const footer_lines = [];
277
+
278
+ if (summary_line.length > 0) {
279
+ footer_lines.push(summary_line);
280
+ }
281
+
282
+ footer_lines.push(...hints.map((hint) => ansi.gray(hint)));
283
+
284
+ return footer_lines.join('\n');
285
+ }
286
+
287
+ /**
288
+ * @param {{ limit: number, offset: number, total_count: number }} summary
289
+ * @returns {boolean}
290
+ */
291
+ function shouldRenderQuerySummary(summary) {
292
+ return summary.offset > 0 || summary.total_count > summary.limit;
293
+ }