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.
- package/lib/build-graph-identity.js +39 -7
- package/lib/build-graph.js +14 -1
- package/lib/cli-help-metadata.js +552 -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 +150 -2
- package/lib/load-patram-config.js +401 -2
- package/lib/load-patram-config.types.ts +31 -0
- package/lib/output-view.types.ts +15 -0
- package/lib/parse-cli-arguments-helpers.js +263 -90
- package/lib/parse-cli-arguments.js +160 -8
- package/lib/parse-cli-arguments.types.ts +48 -3
- package/lib/parse-where-clause.js +604 -209
- package/lib/parse-where-clause.types.ts +70 -0
- package/lib/patram-cli.js +144 -17
- package/lib/patram.js +6 -0
- package/lib/query-graph.js +231 -119
- package/lib/query-inspection.js +523 -0
- package/lib/render-check-output.js +1 -1
- package/lib/render-cli-help.js +419 -0
- package/lib/render-json-output.js +57 -4
- package/lib/render-output-view.js +37 -8
- package/lib/render-plain-output.js +31 -86
- package/lib/render-rich-output.js +34 -87
- package/lib/resolve-where-clause.js +18 -3
- 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/package.json +8 -3
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import {
|
|
3
|
+
* TaggedFencedBlock,
|
|
4
|
+
* TaggedFencedBlockFile,
|
|
5
|
+
* TaggedFencedBlocksInput,
|
|
6
|
+
* } from './tagged-fenced-blocks.types.ts';
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
findMarkdownBodyStartLineIndex,
|
|
11
|
+
getMarkdownTitle,
|
|
12
|
+
isClosingMarkdownFence,
|
|
13
|
+
parseHeading,
|
|
14
|
+
parseOpeningMarkdownFence,
|
|
15
|
+
updateHeadingPath,
|
|
16
|
+
} from './tagged-fenced-block-markdown.js';
|
|
17
|
+
import {
|
|
18
|
+
mergePendingTagSets,
|
|
19
|
+
parseTaggedMetadataLine,
|
|
20
|
+
} from './tagged-fenced-block-metadata.js';
|
|
21
|
+
import { createTaggedFencedBlockError } from './tagged-fenced-block-error.js';
|
|
22
|
+
|
|
23
|
+
const BLANK_LINE_PATTERN = /^\s*$/du;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {{ metadata: Record<string, string>, tag_lines: number[] }} PendingTagSet
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {{
|
|
31
|
+
* heading_path: string[];
|
|
32
|
+
* lang: string;
|
|
33
|
+
* line_start: number;
|
|
34
|
+
* metadata: Record<string, string>;
|
|
35
|
+
* tag_lines: number[];
|
|
36
|
+
* value_lines: string[];
|
|
37
|
+
* }} OpenTaggedBlock
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @typedef {{ character: string, lang: string, length: number }} OpenFence
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {{
|
|
46
|
+
* blocks: TaggedFencedBlock[];
|
|
47
|
+
* body_start: number;
|
|
48
|
+
* heading_path: string[];
|
|
49
|
+
* open_fence: OpenFence | null;
|
|
50
|
+
* open_tagged_block: OpenTaggedBlock | null;
|
|
51
|
+
* pending_tag_set: PendingTagSet | null;
|
|
52
|
+
* title: string;
|
|
53
|
+
* }} TaggedBlockScannerState
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {TaggedFencedBlocksInput} input
|
|
58
|
+
* @returns {TaggedFencedBlockFile}
|
|
59
|
+
*/
|
|
60
|
+
export function extractTaggedFencedBlocksFromSource(input) {
|
|
61
|
+
const lines = input.source_text.split('\n');
|
|
62
|
+
const state = createScannerState(lines);
|
|
63
|
+
|
|
64
|
+
for (const [line_index, line] of lines.entries()) {
|
|
65
|
+
if (line_index < state.body_start) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
scanMarkdownLine(input.file_path, state, line, line_index + 1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
finalizeScannerState(input.file_path, state);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
blocks: state.blocks,
|
|
76
|
+
path: input.file_path,
|
|
77
|
+
title: state.title,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {string} file_path
|
|
83
|
+
* @param {TaggedBlockScannerState} state
|
|
84
|
+
* @param {string} line
|
|
85
|
+
* @param {number} line_number
|
|
86
|
+
*/
|
|
87
|
+
function scanMarkdownLine(file_path, state, line, line_number) {
|
|
88
|
+
if (state.open_fence) {
|
|
89
|
+
scanOpenFenceLine(file_path, state, line, line_number);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (tryOpenFence(state, line, line_number)) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (state.pending_tag_set) {
|
|
98
|
+
scanPendingTagSetLine(file_path, state, line, line_number);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const next_tag_set = parseTaggedMetadataLine(file_path, line, line_number);
|
|
103
|
+
|
|
104
|
+
if (next_tag_set) {
|
|
105
|
+
state.pending_tag_set = next_tag_set;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const heading = parseHeading(line);
|
|
110
|
+
|
|
111
|
+
if (heading) {
|
|
112
|
+
state.heading_path = updateHeadingPath(
|
|
113
|
+
state.heading_path,
|
|
114
|
+
state.title,
|
|
115
|
+
heading,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {string} file_path
|
|
122
|
+
* @param {TaggedBlockScannerState} state
|
|
123
|
+
* @param {string} line
|
|
124
|
+
* @param {number} line_number
|
|
125
|
+
*/
|
|
126
|
+
function scanOpenFenceLine(file_path, state, line, line_number) {
|
|
127
|
+
if (!state.open_fence) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (isClosingMarkdownFence(line, state.open_fence)) {
|
|
132
|
+
if (state.open_tagged_block) {
|
|
133
|
+
state.blocks.push(
|
|
134
|
+
createTaggedBlock(file_path, line_number, state.open_tagged_block),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
state.open_fence = null;
|
|
139
|
+
state.open_tagged_block = null;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (state.open_tagged_block) {
|
|
144
|
+
state.open_tagged_block.value_lines.push(line);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @param {TaggedBlockScannerState} state
|
|
150
|
+
* @param {string} line
|
|
151
|
+
* @param {number} line_number
|
|
152
|
+
* @returns {boolean}
|
|
153
|
+
*/
|
|
154
|
+
function tryOpenFence(state, line, line_number) {
|
|
155
|
+
const open_fence = parseOpeningMarkdownFence(line);
|
|
156
|
+
|
|
157
|
+
if (!open_fence) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
state.open_fence = open_fence;
|
|
162
|
+
state.open_tagged_block = createOpenTaggedBlock(
|
|
163
|
+
line_number,
|
|
164
|
+
open_fence.lang,
|
|
165
|
+
state.pending_tag_set,
|
|
166
|
+
state.heading_path,
|
|
167
|
+
);
|
|
168
|
+
state.pending_tag_set = null;
|
|
169
|
+
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @param {string} file_path
|
|
175
|
+
* @param {TaggedBlockScannerState} state
|
|
176
|
+
* @param {string} line
|
|
177
|
+
* @param {number} line_number
|
|
178
|
+
*/
|
|
179
|
+
function scanPendingTagSetLine(file_path, state, line, line_number) {
|
|
180
|
+
if (!state.pending_tag_set) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (BLANK_LINE_PATTERN.test(line)) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const next_tag_set = parseTaggedMetadataLine(file_path, line, line_number);
|
|
189
|
+
|
|
190
|
+
if (!next_tag_set) {
|
|
191
|
+
throw createTaggedFencedBlockError(
|
|
192
|
+
'tagged_fenced_blocks.unattached_tag_set',
|
|
193
|
+
`Tagged metadata in "${file_path}" at lines ${state.pending_tag_set.tag_lines.join(', ')} must attach directly to the next fenced block.`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
state.pending_tag_set = mergePendingTagSets(
|
|
198
|
+
file_path,
|
|
199
|
+
state.pending_tag_set,
|
|
200
|
+
next_tag_set,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @param {string[]} lines
|
|
206
|
+
* @returns {TaggedBlockScannerState}
|
|
207
|
+
*/
|
|
208
|
+
function createScannerState(lines) {
|
|
209
|
+
const body_start = findMarkdownBodyStartLineIndex(lines);
|
|
210
|
+
const title = getMarkdownTitle(lines, body_start);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
blocks: [],
|
|
214
|
+
body_start,
|
|
215
|
+
heading_path: title.length > 0 ? [title] : [],
|
|
216
|
+
open_fence: null,
|
|
217
|
+
open_tagged_block: null,
|
|
218
|
+
pending_tag_set: null,
|
|
219
|
+
title,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {string} file_path
|
|
225
|
+
* @param {TaggedBlockScannerState} state
|
|
226
|
+
*/
|
|
227
|
+
function finalizeScannerState(file_path, state) {
|
|
228
|
+
if (state.open_tagged_block) {
|
|
229
|
+
throw createTaggedFencedBlockError(
|
|
230
|
+
'tagged_fenced_blocks.unclosed_fence',
|
|
231
|
+
`Unclosed tagged fenced block in "${file_path}" starting at line ${state.open_tagged_block.line_start}.`,
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (state.pending_tag_set) {
|
|
236
|
+
throw createTaggedFencedBlockError(
|
|
237
|
+
'tagged_fenced_blocks.dangling_tag_set',
|
|
238
|
+
`Dangling tagged metadata in "${file_path}" at lines ${state.pending_tag_set.tag_lines.join(', ')}.`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* @param {number} line_number
|
|
245
|
+
* @param {string} lang
|
|
246
|
+
* @param {PendingTagSet | null} pending_tag_set
|
|
247
|
+
* @param {string[]} heading_path
|
|
248
|
+
* @returns {OpenTaggedBlock | null}
|
|
249
|
+
*/
|
|
250
|
+
function createOpenTaggedBlock(
|
|
251
|
+
line_number,
|
|
252
|
+
lang,
|
|
253
|
+
pending_tag_set,
|
|
254
|
+
heading_path,
|
|
255
|
+
) {
|
|
256
|
+
if (!pending_tag_set) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
heading_path: [...heading_path],
|
|
262
|
+
lang,
|
|
263
|
+
line_start: line_number,
|
|
264
|
+
metadata: { ...pending_tag_set.metadata },
|
|
265
|
+
tag_lines: [...pending_tag_set.tag_lines],
|
|
266
|
+
value_lines: [],
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* @param {string} file_path
|
|
272
|
+
* @param {number} line_end
|
|
273
|
+
* @param {OpenTaggedBlock} open_tagged_block
|
|
274
|
+
* @returns {TaggedFencedBlock}
|
|
275
|
+
*/
|
|
276
|
+
function createTaggedBlock(file_path, line_end, open_tagged_block) {
|
|
277
|
+
return {
|
|
278
|
+
context: {
|
|
279
|
+
heading_path: [...open_tagged_block.heading_path],
|
|
280
|
+
},
|
|
281
|
+
id: `block:${file_path}:${open_tagged_block.line_start}`,
|
|
282
|
+
lang: open_tagged_block.lang,
|
|
283
|
+
metadata: { ...open_tagged_block.metadata },
|
|
284
|
+
origin: {
|
|
285
|
+
line_end,
|
|
286
|
+
line_start: open_tagged_block.line_start,
|
|
287
|
+
path: file_path,
|
|
288
|
+
tag_lines: [...open_tagged_block.tag_lines],
|
|
289
|
+
},
|
|
290
|
+
value: open_tagged_block.value_lines.join('\n'),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import {
|
|
3
|
+
* TaggedFencedBlock,
|
|
4
|
+
* TaggedFencedBlockCriteria,
|
|
5
|
+
* TaggedFencedBlockFile,
|
|
6
|
+
* TaggedFencedBlocksInput,
|
|
7
|
+
* } from './tagged-fenced-blocks.types.ts';
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFile } from 'node:fs/promises';
|
|
11
|
+
|
|
12
|
+
import { createTaggedFencedBlockError } from './tagged-fenced-block-error.js';
|
|
13
|
+
import { extractTaggedFencedBlocksFromSource } from './tagged-fenced-block-parser.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Tagged fenced block public API.
|
|
17
|
+
*
|
|
18
|
+
* Loads or extracts one markdown file worth of tagged fenced blocks and
|
|
19
|
+
* provides exact-match selection helpers.
|
|
20
|
+
*
|
|
21
|
+
* Kind: parse
|
|
22
|
+
* Status: active
|
|
23
|
+
* Tracked in: ../docs/plans/v0/tagged-fenced-block-extraction.md
|
|
24
|
+
* Decided by: ../docs/decisions/tagged-fenced-block-extraction.md
|
|
25
|
+
* @patram
|
|
26
|
+
* @see {@link ../docs/decisions/tagged-fenced-block-extraction.md}
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {TaggedFencedBlocksInput} input
|
|
31
|
+
* @returns {TaggedFencedBlockFile}
|
|
32
|
+
*/
|
|
33
|
+
export function extractTaggedFencedBlocks(input) {
|
|
34
|
+
return extractTaggedFencedBlocksFromSource(input);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} file_path
|
|
39
|
+
* @returns {Promise<TaggedFencedBlockFile>}
|
|
40
|
+
*/
|
|
41
|
+
export async function loadTaggedFencedBlocks(file_path) {
|
|
42
|
+
const source_text = await readFile(file_path, 'utf8');
|
|
43
|
+
|
|
44
|
+
return extractTaggedFencedBlocks({
|
|
45
|
+
file_path,
|
|
46
|
+
source_text,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {TaggedFencedBlock[]} blocks
|
|
52
|
+
* @param {TaggedFencedBlockCriteria} criteria
|
|
53
|
+
* @returns {TaggedFencedBlock[]}
|
|
54
|
+
*/
|
|
55
|
+
export function selectTaggedBlocks(blocks, criteria) {
|
|
56
|
+
const criteria_entries = Object.entries(criteria);
|
|
57
|
+
|
|
58
|
+
return blocks.filter((block) =>
|
|
59
|
+
criteria_entries.every(
|
|
60
|
+
([key, value]) =>
|
|
61
|
+
block.metadata[key] !== undefined && block.metadata[key] === value,
|
|
62
|
+
),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {TaggedFencedBlock[]} blocks
|
|
68
|
+
* @param {TaggedFencedBlockCriteria} criteria
|
|
69
|
+
* @returns {TaggedFencedBlock}
|
|
70
|
+
*/
|
|
71
|
+
export function selectTaggedBlock(blocks, criteria) {
|
|
72
|
+
const matches = selectTaggedBlocks(blocks, criteria);
|
|
73
|
+
|
|
74
|
+
if (matches.length === 1) {
|
|
75
|
+
return matches[0];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (matches.length === 0) {
|
|
79
|
+
throw createTaggedFencedBlockError(
|
|
80
|
+
'tagged_fenced_blocks.not_found',
|
|
81
|
+
`No tagged fenced block matches ${formatSelectionCriteria(criteria)}.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
throw createTaggedFencedBlockError(
|
|
86
|
+
'tagged_fenced_blocks.not_unique',
|
|
87
|
+
`Multiple tagged fenced blocks match ${formatSelectionCriteria(criteria)}.`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @param {TaggedFencedBlockCriteria} criteria
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
function formatSelectionCriteria(criteria) {
|
|
96
|
+
return Object.entries(criteria)
|
|
97
|
+
.sort(([left_key], [right_key]) => left_key.localeCompare(right_key))
|
|
98
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
99
|
+
.join(', ');
|
|
100
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface TaggedFencedBlocksInput {
|
|
2
|
+
file_path: string;
|
|
3
|
+
source_text: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface TaggedFencedBlockCriteria {
|
|
7
|
+
[key: string]: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface TaggedFencedBlockOrigin {
|
|
11
|
+
path: string;
|
|
12
|
+
line_start: number;
|
|
13
|
+
line_end: number;
|
|
14
|
+
tag_lines: number[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TaggedFencedBlockContext {
|
|
18
|
+
heading_path: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface TaggedFencedBlock {
|
|
22
|
+
id: string;
|
|
23
|
+
lang: string;
|
|
24
|
+
value: string;
|
|
25
|
+
metadata: Record<string, string>;
|
|
26
|
+
origin: TaggedFencedBlockOrigin;
|
|
27
|
+
context: TaggedFencedBlockContext;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface TaggedFencedBlockFile {
|
|
31
|
+
path: string;
|
|
32
|
+
title: string;
|
|
33
|
+
blocks: TaggedFencedBlock[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface TaggedFencedBlockError extends Error {
|
|
37
|
+
code: string;
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "patram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"main": "./lib/patram.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./lib/patram.js",
|
|
8
|
+
"./bin/patram.js": "./bin/patram.js"
|
|
9
|
+
},
|
|
5
10
|
"files": [
|
|
6
11
|
"bin/patram.js",
|
|
7
12
|
"lib/**/*.js",
|
|
@@ -12,7 +17,7 @@
|
|
|
12
17
|
"!lib/**/*.test-helpers.js"
|
|
13
18
|
],
|
|
14
19
|
"bin": {
|
|
15
|
-
"patram": "
|
|
20
|
+
"patram": "bin/patram.js"
|
|
16
21
|
},
|
|
17
22
|
"homepage": "https://github.com/mantoni/patram",
|
|
18
23
|
"repository": {
|
|
@@ -29,7 +34,7 @@
|
|
|
29
34
|
"check:format": "prettier --check .",
|
|
30
35
|
"check:lint": "eslint .",
|
|
31
36
|
"check:patram": "./bin/patram.js check",
|
|
32
|
-
"check:staged": "lint-staged
|
|
37
|
+
"check:staged": "lint-staged",
|
|
33
38
|
"check:types": "tsc",
|
|
34
39
|
"postversion": "git push && git push --tags",
|
|
35
40
|
"preversion": "npm run all",
|