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
@@ -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.1.1",
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": "./bin/patram.js"
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 --quiet",
37
+ "check:staged": "lint-staged",
33
38
  "check:types": "tsc",
34
39
  "postversion": "git push && git push --tags",
35
40
  "preversion": "npm run all",