patram 0.1.1 → 0.3.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 (49) hide show
  1. package/lib/build-graph-identity.js +57 -24
  2. package/lib/build-graph.js +383 -17
  3. package/lib/build-graph.types.ts +5 -2
  4. package/lib/check-directive-metadata.js +516 -0
  5. package/lib/check-directive-value.js +282 -0
  6. package/lib/check-graph.js +24 -5
  7. package/lib/cli-help-metadata.js +580 -0
  8. package/lib/derived-summary.js +280 -0
  9. package/lib/directive-diagnostics.js +38 -0
  10. package/lib/directive-type-rules.js +133 -0
  11. package/lib/discover-fields.js +427 -0
  12. package/lib/discover-fields.types.ts +52 -0
  13. package/lib/format-derived-summary-row.js +9 -0
  14. package/lib/format-node-header.js +21 -0
  15. package/lib/format-output-item-block.js +22 -0
  16. package/lib/format-output-metadata.js +54 -0
  17. package/lib/layout-stored-queries.js +96 -2
  18. package/lib/load-patram-config.js +754 -18
  19. package/lib/load-patram-config.types.ts +128 -2
  20. package/lib/load-project-graph.js +4 -1
  21. package/lib/output-view.types.ts +29 -6
  22. package/lib/parse-cli-arguments-helpers.js +263 -90
  23. package/lib/parse-cli-arguments.js +160 -8
  24. package/lib/parse-cli-arguments.types.ts +49 -4
  25. package/lib/parse-where-clause.js +670 -209
  26. package/lib/parse-where-clause.types.ts +72 -0
  27. package/lib/patram-cli.js +180 -21
  28. package/lib/patram-config.js +31 -31
  29. package/lib/patram-config.types.ts +10 -4
  30. package/lib/patram.js +6 -0
  31. package/lib/query-graph.js +444 -113
  32. package/lib/query-inspection.js +798 -0
  33. package/lib/render-check-output.js +1 -1
  34. package/lib/render-cli-help.js +419 -0
  35. package/lib/render-field-discovery.js +148 -0
  36. package/lib/render-json-output.js +66 -14
  37. package/lib/render-output-view.js +272 -22
  38. package/lib/render-plain-output.js +31 -86
  39. package/lib/render-rich-output.js +34 -87
  40. package/lib/resolve-patram-graph-config.js +15 -9
  41. package/lib/resolve-where-clause.js +18 -3
  42. package/lib/show-document.js +51 -7
  43. package/lib/tagged-fenced-block-error.js +17 -0
  44. package/lib/tagged-fenced-block-markdown.js +111 -0
  45. package/lib/tagged-fenced-block-metadata.js +97 -0
  46. package/lib/tagged-fenced-block-parser.js +292 -0
  47. package/lib/tagged-fenced-blocks.js +100 -0
  48. package/lib/tagged-fenced-blocks.types.ts +38 -0
  49. package/package.json +12 -7
@@ -21,7 +21,7 @@ import { parsePatramConfig } from './patram-config.js';
21
21
  */
22
22
 
23
23
  const BUILT_IN_PATRAM_CONFIG = {
24
- kinds: {
24
+ classes: {
25
25
  document: {
26
26
  builtin: true,
27
27
  label: 'Document',
@@ -30,28 +30,28 @@ const BUILT_IN_PATRAM_CONFIG = {
30
30
  mappings: {
31
31
  'document.title': {
32
32
  node: {
33
+ class: 'document',
33
34
  field: 'title',
34
- kind: 'document',
35
35
  },
36
36
  },
37
37
  'document.description': {
38
38
  node: {
39
+ class: 'document',
39
40
  field: 'description',
40
- kind: 'document',
41
41
  },
42
42
  },
43
43
  'jsdoc.link': {
44
44
  emit: {
45
45
  relation: 'links_to',
46
46
  target: 'path',
47
- target_kind: 'document',
47
+ target_class: 'document',
48
48
  },
49
49
  },
50
50
  'markdown.link': {
51
51
  emit: {
52
52
  relation: 'links_to',
53
53
  target: 'path',
54
- target_kind: 'document',
54
+ target_class: 'document',
55
55
  },
56
56
  },
57
57
  },
@@ -71,10 +71,10 @@ const BUILT_IN_PATRAM_CONFIG = {
71
71
  * @returns {PatramConfig}
72
72
  */
73
73
  export function resolvePatramGraphConfig(repo_config) {
74
- return parsePatramConfig({
75
- kinds: {
76
- ...BUILT_IN_PATRAM_CONFIG.kinds,
77
- ...repo_config.kinds,
74
+ const graph_config = parsePatramConfig({
75
+ classes: {
76
+ ...BUILT_IN_PATRAM_CONFIG.classes,
77
+ ...repo_config.classes,
78
78
  },
79
79
  mappings: {
80
80
  ...BUILT_IN_PATRAM_CONFIG.mappings,
@@ -85,4 +85,10 @@ export function resolvePatramGraphConfig(repo_config) {
85
85
  ...repo_config.relations,
86
86
  },
87
87
  });
88
+
89
+ return {
90
+ ...graph_config,
91
+ class_schemas: repo_config.class_schemas,
92
+ fields: repo_config.fields,
93
+ };
88
94
  }
@@ -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
  }
@@ -212,17 +212,15 @@ function createResolvedLinkSummary(
212
212
  claim_value.target,
213
213
  );
214
214
  const target_node = graph_nodes[`doc:${target_path}`];
215
- const target_title = target_node?.title ?? claim_value.text;
216
215
 
217
216
  return {
218
217
  label: claim_value.text,
219
218
  reference,
220
- target: {
221
- kind: target_node?.kind,
222
- path: target_node?.path ?? target_path,
223
- status: target_node?.status,
224
- title: target_title,
225
- },
219
+ target: createResolvedLinkTarget(
220
+ target_node,
221
+ target_path,
222
+ claim_value.text,
223
+ ),
226
224
  };
227
225
  }
228
226
 
@@ -239,6 +237,52 @@ function resolveShowTargetPath(source_file_path, raw_target) {
239
237
  return normalizeRepoRelativePath(posix.join(source_directory, raw_target));
240
238
  }
241
239
 
240
+ /**
241
+ * @param {string | string[] | undefined} field_value
242
+ * @returns {string | undefined}
243
+ */
244
+ function getScalarGraphField(field_value) {
245
+ if (Array.isArray(field_value)) {
246
+ return field_value[0];
247
+ }
248
+
249
+ return field_value;
250
+ }
251
+
252
+ /**
253
+ * @param {GraphNode | undefined} target_node
254
+ * @param {string} target_path
255
+ * @param {string} fallback_title
256
+ * @returns {{ kind?: string, path: string, status?: string, title: string }}
257
+ */
258
+ function createResolvedLinkTarget(target_node, target_path, fallback_title) {
259
+ return {
260
+ kind: getResolvedLinkTargetKind(target_node),
261
+ path: getResolvedLinkTargetPath(target_node, target_path),
262
+ status: getScalarGraphField(target_node?.status),
263
+ title: getScalarGraphField(target_node?.title) ?? fallback_title,
264
+ };
265
+ }
266
+
267
+ /**
268
+ * @param {GraphNode | undefined} target_node
269
+ * @returns {string | undefined}
270
+ */
271
+ function getResolvedLinkTargetKind(target_node) {
272
+ return getScalarGraphField(target_node?.$class ?? target_node?.kind);
273
+ }
274
+
275
+ /**
276
+ * @param {GraphNode | undefined} target_node
277
+ * @param {string} target_path
278
+ * @returns {string}
279
+ */
280
+ function getResolvedLinkTargetPath(target_node, target_path) {
281
+ return (
282
+ getScalarGraphField(target_node?.$path ?? target_node?.path) ?? target_path
283
+ );
284
+ }
285
+
242
286
  /**
243
287
  * @param {PatramClaim} claim
244
288
  * @returns {claim is PatramClaim & { type: 'markdown.link', value: { target: string, text: string } }}
@@ -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
+ }
@@ -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
+ }