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
|
@@ -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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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.
|
|
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:
|
|
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
|
}
|
|
@@ -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
|
+
}
|