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.
- package/lib/build-graph-identity.js +57 -24
- package/lib/build-graph.js +383 -17
- package/lib/build-graph.types.ts +5 -2
- package/lib/check-directive-metadata.js +516 -0
- package/lib/check-directive-value.js +282 -0
- package/lib/check-graph.js +24 -5
- package/lib/cli-help-metadata.js +580 -0
- package/lib/derived-summary.js +280 -0
- package/lib/directive-diagnostics.js +38 -0
- package/lib/directive-type-rules.js +133 -0
- package/lib/discover-fields.js +427 -0
- package/lib/discover-fields.types.ts +52 -0
- package/lib/format-derived-summary-row.js +9 -0
- package/lib/format-node-header.js +21 -0
- package/lib/format-output-item-block.js +22 -0
- package/lib/format-output-metadata.js +54 -0
- package/lib/layout-stored-queries.js +96 -2
- package/lib/load-patram-config.js +754 -18
- package/lib/load-patram-config.types.ts +128 -2
- package/lib/load-project-graph.js +4 -1
- package/lib/output-view.types.ts +29 -6
- package/lib/parse-cli-arguments-helpers.js +263 -90
- package/lib/parse-cli-arguments.js +160 -8
- package/lib/parse-cli-arguments.types.ts +49 -4
- package/lib/parse-where-clause.js +670 -209
- package/lib/parse-where-clause.types.ts +72 -0
- package/lib/patram-cli.js +180 -21
- package/lib/patram-config.js +31 -31
- package/lib/patram-config.types.ts +10 -4
- package/lib/patram.js +6 -0
- package/lib/query-graph.js +444 -113
- package/lib/query-inspection.js +798 -0
- package/lib/render-check-output.js +1 -1
- package/lib/render-cli-help.js +419 -0
- package/lib/render-field-discovery.js +148 -0
- package/lib/render-json-output.js +66 -14
- package/lib/render-output-view.js +272 -22
- package/lib/render-plain-output.js +31 -86
- package/lib/render-rich-output.js +34 -87
- package/lib/resolve-patram-graph-config.js +15 -9
- package/lib/resolve-where-clause.js +18 -3
- package/lib/show-document.js +51 -7
- 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 +12 -7
|
@@ -21,7 +21,7 @@ import { parsePatramConfig } from './patram-config.js';
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
const BUILT_IN_PATRAM_CONFIG = {
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
...BUILT_IN_PATRAM_CONFIG.
|
|
77
|
-
...repo_config.
|
|
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:
|
|
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:
|
|
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
|
}
|
package/lib/show-document.js
CHANGED
|
@@ -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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
+
}
|