patram 0.1.1 → 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 (35) hide show
  1. package/lib/build-graph-identity.js +39 -7
  2. package/lib/build-graph.js +14 -1
  3. package/lib/cli-help-metadata.js +552 -0
  4. package/lib/derived-summary.js +278 -0
  5. package/lib/format-derived-summary-row.js +9 -0
  6. package/lib/format-node-header.js +19 -0
  7. package/lib/format-output-item-block.js +22 -0
  8. package/lib/format-output-metadata.js +62 -0
  9. package/lib/layout-stored-queries.js +150 -2
  10. package/lib/load-patram-config.js +401 -2
  11. package/lib/load-patram-config.types.ts +31 -0
  12. package/lib/output-view.types.ts +15 -0
  13. package/lib/parse-cli-arguments-helpers.js +263 -90
  14. package/lib/parse-cli-arguments.js +160 -8
  15. package/lib/parse-cli-arguments.types.ts +48 -3
  16. package/lib/parse-where-clause.js +604 -209
  17. package/lib/parse-where-clause.types.ts +70 -0
  18. package/lib/patram-cli.js +144 -17
  19. package/lib/patram.js +6 -0
  20. package/lib/query-graph.js +231 -119
  21. package/lib/query-inspection.js +523 -0
  22. package/lib/render-check-output.js +1 -1
  23. package/lib/render-cli-help.js +419 -0
  24. package/lib/render-json-output.js +57 -4
  25. package/lib/render-output-view.js +37 -8
  26. package/lib/render-plain-output.js +31 -86
  27. package/lib/render-rich-output.js +34 -87
  28. package/lib/resolve-where-clause.js +18 -3
  29. package/lib/tagged-fenced-block-error.js +17 -0
  30. package/lib/tagged-fenced-block-markdown.js +111 -0
  31. package/lib/tagged-fenced-block-metadata.js +97 -0
  32. package/lib/tagged-fenced-block-parser.js +292 -0
  33. package/lib/tagged-fenced-blocks.js +100 -0
  34. package/lib/tagged-fenced-blocks.types.ts +38 -0
  35. package/package.json +8 -3
@@ -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}] document ${output_item.target.path}`,
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(`document ${output_item.target.path}`)}`,
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}
@@ -2,12 +2,16 @@
2
2
  * @import { PatramRepoConfig } from './load-patram-config.types.ts';
3
3
  */
4
4
 
5
+ /**
6
+ * @typedef {{ kind: 'ad_hoc' } | { kind: 'stored_query', name: string }} QuerySource
7
+ */
8
+
5
9
  /**
6
10
  * Resolve an ad hoc or stored query into a where clause.
7
11
  *
8
12
  * @param {PatramRepoConfig} repo_config
9
13
  * @param {string[]} command_arguments
10
- * @returns {{ success: true, value: string } | { success: false, message: string }}
14
+ * @returns {{ success: true, value: { query_source: QuerySource, where_clause: string } } | { success: false, message: string }}
11
15
  */
12
16
  export function resolveWhereClause(repo_config, command_arguments) {
13
17
  if (command_arguments[0] === '--where') {
@@ -22,7 +26,12 @@ export function resolveWhereClause(repo_config, command_arguments) {
22
26
 
23
27
  return {
24
28
  success: true,
25
- value: where_clause,
29
+ value: {
30
+ query_source: {
31
+ kind: 'ad_hoc',
32
+ },
33
+ where_clause,
34
+ },
26
35
  };
27
36
  }
28
37
 
@@ -46,6 +55,12 @@ export function resolveWhereClause(repo_config, command_arguments) {
46
55
 
47
56
  return {
48
57
  success: true,
49
- value: stored_query.where,
58
+ value: {
59
+ query_source: {
60
+ kind: 'stored_query',
61
+ name: stored_query_name,
62
+ },
63
+ where_clause: stored_query.where,
64
+ },
50
65
  };
51
66
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @import { TaggedFencedBlockError } from './tagged-fenced-blocks.types.ts';
3
+ */
4
+
5
+ /**
6
+ * @param {string} code
7
+ * @param {string} message
8
+ * @returns {TaggedFencedBlockError}
9
+ */
10
+ export function createTaggedFencedBlockError(code, message) {
11
+ const error = /** @type {TaggedFencedBlockError} */ (new Error(message));
12
+
13
+ error.code = code;
14
+ error.name = 'TaggedFencedBlockError';
15
+
16
+ return error;
17
+ }
@@ -0,0 +1,111 @@
1
+ const FRONT_MATTER_BOUNDARY_PATTERN = /^---$/du;
2
+ const HEADING_PATTERN = /^(#{1,6})\s+(.+?)(?:\s+#+\s*)?$/du;
3
+ const MARKDOWN_FENCE_PATTERN = /^([`~]{3,})(.*)$/du;
4
+
5
+ /**
6
+ * @param {string[]} lines
7
+ * @returns {number}
8
+ */
9
+ export function findMarkdownBodyStartLineIndex(lines) {
10
+ if (lines[0] !== '---') {
11
+ return 0;
12
+ }
13
+
14
+ for (let line_index = 1; line_index < lines.length; line_index += 1) {
15
+ if (FRONT_MATTER_BOUNDARY_PATTERN.test(lines[line_index])) {
16
+ return line_index + 1;
17
+ }
18
+ }
19
+
20
+ return 0;
21
+ }
22
+
23
+ /**
24
+ * @param {string[]} lines
25
+ * @param {number} body_start
26
+ * @returns {string}
27
+ */
28
+ export function getMarkdownTitle(lines, body_start) {
29
+ const title_line = lines[body_start];
30
+
31
+ if (title_line === undefined) {
32
+ return '';
33
+ }
34
+
35
+ const trimmed_line = title_line.trim();
36
+
37
+ if (trimmed_line.length === 0) {
38
+ return '';
39
+ }
40
+
41
+ return parseHeading(trimmed_line)?.text ?? trimmed_line;
42
+ }
43
+
44
+ /**
45
+ * @param {string} line
46
+ * @returns {{ level: number, text: string } | null}
47
+ */
48
+ export function parseHeading(line) {
49
+ const heading_match = line.trim().match(HEADING_PATTERN);
50
+
51
+ if (!heading_match) {
52
+ return null;
53
+ }
54
+
55
+ return {
56
+ level: heading_match[1].length,
57
+ text: heading_match[2].trim(),
58
+ };
59
+ }
60
+
61
+ /**
62
+ * @param {string[]} heading_path
63
+ * @param {string} title
64
+ * @param {{ level: number, text: string }} heading
65
+ * @returns {string[]}
66
+ */
67
+ export function updateHeadingPath(heading_path, title, heading) {
68
+ if (heading.level === 1) {
69
+ return [heading.text];
70
+ }
71
+
72
+ const next_heading_path = heading_path.slice(0, heading.level - 1);
73
+
74
+ if (next_heading_path.length === 0 && title.length > 0) {
75
+ next_heading_path.push(title);
76
+ }
77
+
78
+ next_heading_path.push(heading.text);
79
+
80
+ return next_heading_path;
81
+ }
82
+
83
+ /**
84
+ * @param {string} line
85
+ * @returns {{ character: string, lang: string, length: number } | null}
86
+ */
87
+ export function parseOpeningMarkdownFence(line) {
88
+ const trimmed_line = line.trimStart();
89
+ const fence_match = trimmed_line.match(MARKDOWN_FENCE_PATTERN);
90
+
91
+ if (!fence_match) {
92
+ return null;
93
+ }
94
+
95
+ return {
96
+ character: fence_match[1][0],
97
+ lang: fence_match[2].trim(),
98
+ length: fence_match[1].length,
99
+ };
100
+ }
101
+
102
+ /**
103
+ * @param {string} line
104
+ * @param {{ character: string, length: number }} open_fence
105
+ * @returns {boolean}
106
+ */
107
+ export function isClosingMarkdownFence(line, open_fence) {
108
+ return line
109
+ .trimStart()
110
+ .startsWith(open_fence.character.repeat(open_fence.length));
111
+ }
@@ -0,0 +1,97 @@
1
+ import { createTaggedFencedBlockError } from './tagged-fenced-block-error.js';
2
+
3
+ const TAGGED_METADATA_LINE_PATTERN = /^\[patram\s+(.+)\]:\s*#\s*$/du;
4
+ const TAGGED_METADATA_PAIR_PATTERN = /^([a-z][a-z0-9_]*)=([^\s]+)$/du;
5
+
6
+ /**
7
+ * @param {string} file_path
8
+ * @param {{ metadata: Record<string, string>, tag_lines: number[] }} pending_tag_set
9
+ * @param {{ metadata: Record<string, string>, tag_lines: number[] }} next_tag_set
10
+ * @returns {{ metadata: Record<string, string>, tag_lines: number[] }}
11
+ */
12
+ export function mergePendingTagSets(file_path, pending_tag_set, next_tag_set) {
13
+ /** @type {Record<string, string>} */
14
+ const metadata = { ...pending_tag_set.metadata };
15
+
16
+ for (const [key, value] of Object.entries(next_tag_set.metadata)) {
17
+ if (metadata[key] !== undefined) {
18
+ throw createTaggedFencedBlockError(
19
+ 'tagged_fenced_blocks.duplicate_metadata_key',
20
+ `Duplicate tagged metadata key "${key}" in "${file_path}" at line ${next_tag_set.tag_lines[0]}.`,
21
+ );
22
+ }
23
+
24
+ metadata[key] = value;
25
+ }
26
+
27
+ return {
28
+ metadata,
29
+ tag_lines: [...pending_tag_set.tag_lines, ...next_tag_set.tag_lines],
30
+ };
31
+ }
32
+
33
+ /**
34
+ * @param {string} file_path
35
+ * @param {string} line
36
+ * @param {number} line_number
37
+ * @returns {{ metadata: Record<string, string>, tag_lines: number[] } | null}
38
+ */
39
+ export function parseTaggedMetadataLine(file_path, line, line_number) {
40
+ const trimmed_line = line.trim();
41
+
42
+ if (!trimmed_line.startsWith('[patram')) {
43
+ return null;
44
+ }
45
+
46
+ const metadata_match = trimmed_line.match(TAGGED_METADATA_LINE_PATTERN);
47
+
48
+ if (!metadata_match) {
49
+ throw createTaggedFencedBlockError(
50
+ 'tagged_fenced_blocks.invalid_tag_line',
51
+ `Invalid tagged metadata line in "${file_path}" at line ${line_number}.`,
52
+ );
53
+ }
54
+
55
+ return {
56
+ metadata: parseTaggedMetadataPairs(
57
+ file_path,
58
+ metadata_match[1],
59
+ line_number,
60
+ ),
61
+ tag_lines: [line_number],
62
+ };
63
+ }
64
+
65
+ /**
66
+ * @param {string} file_path
67
+ * @param {string} pair_text
68
+ * @param {number} line_number
69
+ * @returns {Record<string, string>}
70
+ */
71
+ function parseTaggedMetadataPairs(file_path, pair_text, line_number) {
72
+ const tokens = pair_text.split(/\s+/du);
73
+ /** @type {Record<string, string>} */
74
+ const metadata = {};
75
+
76
+ for (const token of tokens) {
77
+ const pair_match = token.match(TAGGED_METADATA_PAIR_PATTERN);
78
+
79
+ if (!pair_match) {
80
+ throw createTaggedFencedBlockError(
81
+ 'tagged_fenced_blocks.invalid_tag_line',
82
+ `Invalid tagged metadata line in "${file_path}" at line ${line_number}.`,
83
+ );
84
+ }
85
+
86
+ if (metadata[pair_match[1]] !== undefined) {
87
+ throw createTaggedFencedBlockError(
88
+ 'tagged_fenced_blocks.duplicate_metadata_key',
89
+ `Duplicate tagged metadata key "${pair_match[1]}" in "${file_path}" at line ${line_number}.`,
90
+ );
91
+ }
92
+
93
+ metadata[pair_match[1]] = pair_match[2];
94
+ }
95
+
96
+ return metadata;
97
+ }