patram 0.0.2 → 0.1.1
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/bin/patram.js +25 -147
- package/lib/build-graph-identity.js +238 -0
- package/lib/build-graph.js +143 -77
- package/lib/check-graph.js +23 -7
- package/lib/claim-helpers.js +55 -0
- package/lib/command-output.js +83 -0
- package/lib/layout-stored-queries.js +213 -0
- package/lib/list-queries.js +18 -0
- package/lib/list-source-files.js +50 -15
- package/lib/load-patram-config.js +106 -18
- package/lib/load-patram-config.types.ts +9 -0
- package/lib/load-project-graph.js +124 -0
- package/lib/output-view.types.ts +73 -0
- package/lib/parse-claims.js +38 -158
- package/lib/parse-claims.types.ts +7 -0
- package/lib/parse-cli-arguments-helpers.js +273 -0
- package/lib/parse-cli-arguments.js +114 -0
- package/lib/parse-cli-arguments.types.ts +24 -0
- package/lib/parse-cli-color-options.js +44 -0
- package/lib/parse-cli-query-pagination.js +49 -0
- package/lib/parse-jsdoc-blocks.js +184 -0
- package/lib/parse-jsdoc-claims.js +280 -0
- package/lib/parse-jsdoc-prose.js +111 -0
- package/lib/parse-markdown-claims.js +242 -0
- package/lib/parse-markdown-directives.js +136 -0
- package/lib/parse-where-clause.js +312 -0
- package/lib/patram-cli.js +337 -0
- package/lib/patram-config.js +3 -1
- package/lib/patram-config.types.ts +2 -1
- package/lib/query-graph.js +256 -0
- package/lib/render-check-output.js +315 -0
- package/lib/render-json-output.js +108 -0
- package/lib/render-output-view.js +193 -0
- package/lib/render-plain-output.js +237 -0
- package/lib/render-rich-output.js +293 -0
- package/lib/render-rich-source.js +1333 -0
- package/lib/resolve-check-target.js +190 -0
- package/lib/resolve-output-mode.js +60 -0
- package/lib/resolve-patram-graph-config.js +88 -0
- package/lib/resolve-where-clause.js +51 -0
- package/lib/show-document.js +311 -0
- package/lib/source-file-defaults.js +28 -0
- package/lib/write-paged-output.js +87 -0
- package/package.json +21 -10
- package/bin/patram.test.js +0 -184
- package/lib/build-graph.test.js +0 -141
- package/lib/check-graph.test.js +0 -103
- package/lib/list-source-files.test.js +0 -101
- package/lib/load-patram-config.test.js +0 -211
- package/lib/parse-claims.test.js +0 -113
- package/lib/patram-config.test.js +0 -147
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/** @import * as $k$$l$load$j$patram$j$config$k$types$k$ts from './load-patram-config.types.ts'; */
|
|
2
|
+
/**
|
|
3
|
+
* @import { ParseClaimsInput, ParseSourceFileResult, PatramClaim, PatramClaimFields } from './parse-claims.types.ts';
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
createClaim,
|
|
8
|
+
getFileExtension,
|
|
9
|
+
isPathLikeTarget,
|
|
10
|
+
} from './claim-helpers.js';
|
|
11
|
+
import { normalizeDirectiveName } from './parse-markdown-directives.js';
|
|
12
|
+
import { collectJsdocBlocks } from './parse-jsdoc-blocks.js';
|
|
13
|
+
import {
|
|
14
|
+
createJsdocProseClaimEntries,
|
|
15
|
+
pushJsdocParagraph,
|
|
16
|
+
} from './parse-jsdoc-prose.js';
|
|
17
|
+
import { JSDOC_SOURCE_FILE_EXTENSIONS } from './source-file-defaults.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* JSDoc @patram claim parsing.
|
|
21
|
+
*
|
|
22
|
+
* Activates one source anchor block, extracts directives and links, and
|
|
23
|
+
* derives title and description claims from prose.
|
|
24
|
+
*
|
|
25
|
+
* Kind: parse
|
|
26
|
+
* Status: active
|
|
27
|
+
* Tracked in: ../docs/plans/v0/source-anchor-dogfooding.md
|
|
28
|
+
* Decided by: ../docs/decisions/jsdoc-metadata-directive-syntax.md
|
|
29
|
+
* @patram
|
|
30
|
+
* @see {@link ./parse-claims.js}
|
|
31
|
+
* @see {@link ../docs/decisions/jsdoc-metadata-directive-syntax.md}
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
const JSDOC_EXTENSIONS = new Set(JSDOC_SOURCE_FILE_EXTENSIONS);
|
|
35
|
+
const JSDOC_LINK_TAG_PATTERN = /^@(link|see)\s+(.+)$/du;
|
|
36
|
+
const JSDOC_TAG_PATTERN = /^@[A-Za-z]+(?:\s|$)/du;
|
|
37
|
+
const JSDOC_VISIBLE_DIRECTIVE_PATTERN = /^([A-Z][A-Za-z _-]*):\s+(.+)$/du;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse JSDoc metadata claims from one source file.
|
|
41
|
+
*
|
|
42
|
+
* @param {ParseClaimsInput} parse_input
|
|
43
|
+
* @returns {ParseSourceFileResult}
|
|
44
|
+
*/
|
|
45
|
+
export function parseJsdocClaims(parse_input) {
|
|
46
|
+
if (!JSDOC_EXTENSIONS.has(getFileExtension(parse_input.path))) {
|
|
47
|
+
return {
|
|
48
|
+
claims: [],
|
|
49
|
+
diagnostics: [],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const jsdoc_blocks = collectJsdocBlocks(parse_input.source);
|
|
54
|
+
const active_blocks = jsdoc_blocks.filter(
|
|
55
|
+
(jsdoc_block) => jsdoc_block.activation_line !== null,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (active_blocks.length === 0) {
|
|
59
|
+
return {
|
|
60
|
+
claims: [],
|
|
61
|
+
diagnostics: [],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const claim_entries = collectJsdocClaimEntries(
|
|
66
|
+
parse_input.path,
|
|
67
|
+
active_blocks[0],
|
|
68
|
+
).sort(compareClaimEntries);
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
claims: claim_entries.map((claim_entry, claim_index) =>
|
|
72
|
+
createClaim(
|
|
73
|
+
parse_input.path,
|
|
74
|
+
claim_index + 1,
|
|
75
|
+
claim_entry.claim_type,
|
|
76
|
+
claim_entry.claim_fields,
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
diagnostics: active_blocks
|
|
80
|
+
.slice(1)
|
|
81
|
+
.map((active_block) =>
|
|
82
|
+
createMultiplePatramBlocksDiagnostic(parse_input.path, active_block),
|
|
83
|
+
),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {string} file_path
|
|
89
|
+
* @param {{ lines: Array<{ column: number, content: string, line: number }> }} jsdoc_block
|
|
90
|
+
* @returns {Array<{ claim_fields: PatramClaimFields, claim_type: string, order: number }>}
|
|
91
|
+
*/
|
|
92
|
+
function collectJsdocClaimEntries(file_path, jsdoc_block) {
|
|
93
|
+
/** @type {Array<{ claim_fields: PatramClaimFields, claim_type: string, order: number }>} */
|
|
94
|
+
const claim_entries = [];
|
|
95
|
+
/** @type {Array<Array<{ column: number, content: string, line: number }>>} */
|
|
96
|
+
const prose_paragraphs = [];
|
|
97
|
+
/** @type {Array<{ column: number, content: string, line: number }>} */
|
|
98
|
+
let current_paragraph_lines = [];
|
|
99
|
+
|
|
100
|
+
for (const block_line of jsdoc_block.lines) {
|
|
101
|
+
if (block_line.content.length === 0) {
|
|
102
|
+
pushJsdocParagraph(prose_paragraphs, current_paragraph_lines);
|
|
103
|
+
current_paragraph_lines = [];
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const directive_fields = matchJsdocDirectiveFields(file_path, block_line);
|
|
108
|
+
|
|
109
|
+
if (directive_fields) {
|
|
110
|
+
pushJsdocParagraph(prose_paragraphs, current_paragraph_lines);
|
|
111
|
+
current_paragraph_lines = [];
|
|
112
|
+
claim_entries.push({
|
|
113
|
+
claim_fields: directive_fields,
|
|
114
|
+
claim_type: 'directive',
|
|
115
|
+
order: 0,
|
|
116
|
+
});
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const jsdoc_link_claim_entry = createJsdocLinkClaimEntry(
|
|
121
|
+
file_path,
|
|
122
|
+
block_line,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
if (jsdoc_link_claim_entry) {
|
|
126
|
+
pushJsdocParagraph(prose_paragraphs, current_paragraph_lines);
|
|
127
|
+
current_paragraph_lines = [];
|
|
128
|
+
claim_entries.push(jsdoc_link_claim_entry);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (JSDOC_TAG_PATTERN.test(block_line.content)) {
|
|
133
|
+
pushJsdocParagraph(prose_paragraphs, current_paragraph_lines);
|
|
134
|
+
current_paragraph_lines = [];
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
current_paragraph_lines.push(block_line);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
pushJsdocParagraph(prose_paragraphs, current_paragraph_lines);
|
|
142
|
+
claim_entries.push(
|
|
143
|
+
...createJsdocProseClaimEntries(file_path, prose_paragraphs),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
return claim_entries;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {string} file_path
|
|
151
|
+
* @param {{ column: number, content: string, line: number }} block_line
|
|
152
|
+
* @returns {PatramClaimFields | null}
|
|
153
|
+
*/
|
|
154
|
+
function matchJsdocDirectiveFields(file_path, block_line) {
|
|
155
|
+
const directive_match = block_line.content.match(
|
|
156
|
+
JSDOC_VISIBLE_DIRECTIVE_PATTERN,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (!directive_match) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
name: normalizeDirectiveName(directive_match[1]),
|
|
165
|
+
origin: {
|
|
166
|
+
column: block_line.column,
|
|
167
|
+
line: block_line.line,
|
|
168
|
+
path: file_path,
|
|
169
|
+
},
|
|
170
|
+
parser: 'jsdoc',
|
|
171
|
+
value: directive_match[2].trim(),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* @param {string} file_path
|
|
177
|
+
* @param {{ column: number, content: string, line: number }} block_line
|
|
178
|
+
* @returns {{ claim_fields: PatramClaimFields, claim_type: string, order: number } | null}
|
|
179
|
+
*/
|
|
180
|
+
function createJsdocLinkClaimEntry(file_path, block_line) {
|
|
181
|
+
const link_match = block_line.content.match(JSDOC_LINK_TAG_PATTERN);
|
|
182
|
+
|
|
183
|
+
if (!link_match) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const link_value = parseJsdocLinkValue(link_match[2].trim());
|
|
188
|
+
|
|
189
|
+
if (!link_value) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
claim_fields: {
|
|
195
|
+
origin: {
|
|
196
|
+
column: block_line.column,
|
|
197
|
+
line: block_line.line,
|
|
198
|
+
path: file_path,
|
|
199
|
+
},
|
|
200
|
+
value: link_value,
|
|
201
|
+
},
|
|
202
|
+
claim_type: 'jsdoc.link',
|
|
203
|
+
order: 0,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* @param {string} raw_value
|
|
209
|
+
* @returns {{ target: string, text: string } | null}
|
|
210
|
+
*/
|
|
211
|
+
function parseJsdocLinkValue(raw_value) {
|
|
212
|
+
const inline_link_match = raw_value.match(
|
|
213
|
+
/^\{@link\s+([^}\s]+)(?:\s+([^}]+))?\}$/du,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
if (inline_link_match) {
|
|
217
|
+
const target_value = inline_link_match[1];
|
|
218
|
+
const label_value = inline_link_match[2]?.trim();
|
|
219
|
+
|
|
220
|
+
if (!isPathLikeTarget(target_value)) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
target: target_value,
|
|
226
|
+
text: label_value && label_value.length > 0 ? label_value : target_value,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const [target_value, ...label_parts] = raw_value.split(/\s+/du);
|
|
231
|
+
|
|
232
|
+
if (!target_value || !isPathLikeTarget(target_value)) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
target: target_value,
|
|
238
|
+
text: label_parts.length > 0 ? label_parts.join(' ') : target_value,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @param {{ claim_fields: PatramClaimFields, claim_type: string, order: number }} left_entry
|
|
244
|
+
* @param {{ claim_fields: PatramClaimFields, claim_type: string, order: number }} right_entry
|
|
245
|
+
* @returns {number}
|
|
246
|
+
*/
|
|
247
|
+
function compareClaimEntries(left_entry, right_entry) {
|
|
248
|
+
const left_origin = left_entry.claim_fields.origin;
|
|
249
|
+
const right_origin = right_entry.claim_fields.origin;
|
|
250
|
+
|
|
251
|
+
if (!left_origin || !right_origin) {
|
|
252
|
+
return left_entry.order - right_entry.order;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (left_origin.line !== right_origin.line) {
|
|
256
|
+
return left_origin.line - right_origin.line;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (left_origin.column !== right_origin.column) {
|
|
260
|
+
return left_origin.column - right_origin.column;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return left_entry.order - right_entry.order;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* @param {string} file_path
|
|
268
|
+
* @param {{ activation_column: number | null, activation_line: number | null }} jsdoc_block
|
|
269
|
+
* @returns {$k$$l$load$j$patram$j$config$k$types$k$ts.PatramDiagnostic}
|
|
270
|
+
*/
|
|
271
|
+
function createMultiplePatramBlocksDiagnostic(file_path, jsdoc_block) {
|
|
272
|
+
return {
|
|
273
|
+
code: 'jsdoc.multiple_patram_blocks',
|
|
274
|
+
column: jsdoc_block.activation_column ?? 1,
|
|
275
|
+
level: 'error',
|
|
276
|
+
line: jsdoc_block.activation_line ?? 1,
|
|
277
|
+
message: `File "${file_path}" contains multiple JSDoc blocks with "@patram".`,
|
|
278
|
+
path: file_path,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { PatramClaimFields } from './parse-claims.types.ts';
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const JSDOC_SENTENCE_PATTERN = /^(.+?[.!?])(?:\s+|$)([\s\S]*)$/du;
|
|
6
|
+
const JSDOC_TITLE_LENGTH_LIMIT = 120;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {Array<Array<{ column: number, content: string, line: number }>>} prose_paragraphs
|
|
10
|
+
* @param {Array<{ column: number, content: string, line: number }>} paragraph_lines
|
|
11
|
+
*/
|
|
12
|
+
export function pushJsdocParagraph(prose_paragraphs, paragraph_lines) {
|
|
13
|
+
if (paragraph_lines.length > 0) {
|
|
14
|
+
prose_paragraphs.push(paragraph_lines);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} file_path
|
|
20
|
+
* @param {Array<Array<{ column: number, content: string, line: number }>>} prose_paragraphs
|
|
21
|
+
* @returns {Array<{ claim_fields: PatramClaimFields, claim_type: string, order: number }>}
|
|
22
|
+
*/
|
|
23
|
+
export function createJsdocProseClaimEntries(file_path, prose_paragraphs) {
|
|
24
|
+
if (prose_paragraphs.length === 0) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const first_paragraph = prose_paragraphs[0];
|
|
29
|
+
const first_origin = {
|
|
30
|
+
column: first_paragraph[0].column,
|
|
31
|
+
line: first_paragraph[0].line,
|
|
32
|
+
path: file_path,
|
|
33
|
+
};
|
|
34
|
+
const title_result = splitJsdocParagraphTitle(
|
|
35
|
+
first_paragraph.map((line) => line.content).join(' '),
|
|
36
|
+
);
|
|
37
|
+
/** @type {Array<{ claim_fields: PatramClaimFields, claim_type: string, order: number }>} */
|
|
38
|
+
const claim_entries = [
|
|
39
|
+
{
|
|
40
|
+
claim_fields: {
|
|
41
|
+
origin: first_origin,
|
|
42
|
+
value: title_result.title,
|
|
43
|
+
},
|
|
44
|
+
claim_type: 'document.title',
|
|
45
|
+
order: 0,
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
/** @type {string[]} */
|
|
49
|
+
const description_parts = [];
|
|
50
|
+
/** @type {{ column: number, line: number, path: string } | null} */
|
|
51
|
+
let description_origin = null;
|
|
52
|
+
|
|
53
|
+
if (title_result.remainder.length > 0) {
|
|
54
|
+
description_parts.push(title_result.remainder);
|
|
55
|
+
description_origin = first_origin;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const paragraph_lines of prose_paragraphs.slice(1)) {
|
|
59
|
+
if (description_origin === null) {
|
|
60
|
+
description_origin = {
|
|
61
|
+
column: paragraph_lines[0].column,
|
|
62
|
+
line: paragraph_lines[0].line,
|
|
63
|
+
path: file_path,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
description_parts.push(
|
|
68
|
+
paragraph_lines.map((line) => line.content).join(' '),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (description_origin && description_parts.length > 0) {
|
|
73
|
+
claim_entries.push({
|
|
74
|
+
claim_fields: {
|
|
75
|
+
origin: description_origin,
|
|
76
|
+
value: description_parts.join('\n\n'),
|
|
77
|
+
},
|
|
78
|
+
claim_type: 'document.description',
|
|
79
|
+
order: 1,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return claim_entries;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {string} paragraph_text
|
|
88
|
+
* @returns {{ remainder: string, title: string }}
|
|
89
|
+
*/
|
|
90
|
+
function splitJsdocParagraphTitle(paragraph_text) {
|
|
91
|
+
if (paragraph_text.length <= JSDOC_TITLE_LENGTH_LIMIT) {
|
|
92
|
+
return {
|
|
93
|
+
remainder: '',
|
|
94
|
+
title: paragraph_text,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const sentence_match = paragraph_text.match(JSDOC_SENTENCE_PATTERN);
|
|
99
|
+
|
|
100
|
+
if (!sentence_match) {
|
|
101
|
+
return {
|
|
102
|
+
remainder: '',
|
|
103
|
+
title: paragraph_text,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
remainder: sentence_match[2].trim(),
|
|
109
|
+
title: sentence_match[1].trim(),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { PatramClaim, ParseClaimsInput, PatramClaimFields } from './parse-claims.types.ts';
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createClaim, isPathLikeTarget } from './claim-helpers.js';
|
|
6
|
+
import {
|
|
7
|
+
matchHiddenDirectiveFields,
|
|
8
|
+
matchVisibleDirectiveFields,
|
|
9
|
+
parseFrontMatterDirectiveFields,
|
|
10
|
+
} from './parse-markdown-directives.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Markdown claim parsing.
|
|
14
|
+
*
|
|
15
|
+
* Extracts document titles, directives, and links from markdown source while
|
|
16
|
+
* ignoring fenced-code link noise.
|
|
17
|
+
*
|
|
18
|
+
* Kind: parse
|
|
19
|
+
* Status: active
|
|
20
|
+
* Tracked in: ../docs/plans/v0/source-anchor-dogfooding.md
|
|
21
|
+
* Decided by: ../docs/decisions/markdown-metadata-directive-syntax.md
|
|
22
|
+
* Decided by: ../docs/decisions/markdown-link-claim-scope.md
|
|
23
|
+
* @patram
|
|
24
|
+
* @see {@link ./parse-claims.js}
|
|
25
|
+
* @see {@link ../docs/decisions/markdown-metadata-directive-syntax.md}
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const HEADING_PATTERN = /^#\s+(.+)$/du;
|
|
29
|
+
const MARKDOWN_FENCE_PATTERN = /^([`~]{3,})/du;
|
|
30
|
+
const MARKDOWN_LINK_PATTERN = /\[([^\]]+)\]\(([^)]+)\)/dgu;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {ParseClaimsInput} parse_input
|
|
34
|
+
* @returns {PatramClaim[]}
|
|
35
|
+
*/
|
|
36
|
+
export function parseMarkdownClaims(parse_input) {
|
|
37
|
+
const lines = parse_input.source.split('\n');
|
|
38
|
+
|
|
39
|
+
/** @type {PatramClaim[]} */
|
|
40
|
+
const claims = [];
|
|
41
|
+
const front_matter_result = parseFrontMatterDirectiveFields(
|
|
42
|
+
parse_input.path,
|
|
43
|
+
lines,
|
|
44
|
+
);
|
|
45
|
+
/** @type {{ character: string, length: number } | null} */
|
|
46
|
+
let open_fence = null;
|
|
47
|
+
const title_result = getMarkdownTitle(lines, front_matter_result.body_start);
|
|
48
|
+
|
|
49
|
+
if (title_result) {
|
|
50
|
+
claims.push(
|
|
51
|
+
createClaim(parse_input.path, claims.length + 1, 'document.title', {
|
|
52
|
+
origin: {
|
|
53
|
+
column: 1,
|
|
54
|
+
line: title_result.line,
|
|
55
|
+
path: parse_input.path,
|
|
56
|
+
},
|
|
57
|
+
value: title_result.value,
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const directive_fields of front_matter_result.directive_fields) {
|
|
63
|
+
claims.push(
|
|
64
|
+
createClaim(parse_input.path, claims.length + 1, 'directive', {
|
|
65
|
+
...directive_fields,
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const [line_index, line] of lines.entries()) {
|
|
71
|
+
if (line_index < front_matter_result.body_start) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const line_number = line_index + 1;
|
|
76
|
+
|
|
77
|
+
if (open_fence) {
|
|
78
|
+
if (isClosingMarkdownFence(line, open_fence)) {
|
|
79
|
+
open_fence = null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
open_fence = parseOpeningMarkdownFence(line);
|
|
86
|
+
|
|
87
|
+
if (open_fence) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
collectMarkdownLinkClaims(parse_input.path, line, line_number, claims);
|
|
92
|
+
collectVisibleDirectiveClaims(parse_input.path, line, line_number, claims);
|
|
93
|
+
collectHiddenDirectiveClaims(parse_input.path, line, line_number, claims);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return claims;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {string} file_path
|
|
101
|
+
* @param {string} line
|
|
102
|
+
* @param {number} line_number
|
|
103
|
+
* @param {PatramClaim[]} claims
|
|
104
|
+
*/
|
|
105
|
+
function collectMarkdownLinkClaims(file_path, line, line_number, claims) {
|
|
106
|
+
for (const link_match of line.matchAll(MARKDOWN_LINK_PATTERN)) {
|
|
107
|
+
const link_text = link_match[1];
|
|
108
|
+
const target_value = link_match[2];
|
|
109
|
+
|
|
110
|
+
if (!isPathLikeTarget(target_value)) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const column_number =
|
|
115
|
+
link_match.index === undefined ? 1 : link_match.index + 1;
|
|
116
|
+
|
|
117
|
+
claims.push(
|
|
118
|
+
createClaim(file_path, claims.length + 1, 'markdown.link', {
|
|
119
|
+
origin: {
|
|
120
|
+
column: column_number,
|
|
121
|
+
line: line_number,
|
|
122
|
+
path: file_path,
|
|
123
|
+
},
|
|
124
|
+
value: {
|
|
125
|
+
target: target_value,
|
|
126
|
+
text: link_text,
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* @param {string} file_path
|
|
135
|
+
* @param {string} line
|
|
136
|
+
* @param {number} line_number
|
|
137
|
+
* @param {PatramClaim[]} claims
|
|
138
|
+
*/
|
|
139
|
+
function collectVisibleDirectiveClaims(file_path, line, line_number, claims) {
|
|
140
|
+
const directive_fields = matchVisibleDirectiveFields(
|
|
141
|
+
file_path,
|
|
142
|
+
line,
|
|
143
|
+
line_number,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (directive_fields) {
|
|
147
|
+
pushDirectiveClaim(file_path, claims, directive_fields);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {string} file_path
|
|
153
|
+
* @param {string} line
|
|
154
|
+
* @param {number} line_number
|
|
155
|
+
* @param {PatramClaim[]} claims
|
|
156
|
+
*/
|
|
157
|
+
function collectHiddenDirectiveClaims(file_path, line, line_number, claims) {
|
|
158
|
+
const directive_fields = matchHiddenDirectiveFields(
|
|
159
|
+
file_path,
|
|
160
|
+
line,
|
|
161
|
+
line_number,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
if (directive_fields) {
|
|
165
|
+
pushDirectiveClaim(file_path, claims, directive_fields);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* @param {string[]} lines
|
|
171
|
+
* @param {number} start_line_index
|
|
172
|
+
* @returns {{ line: number, value: string } | null}
|
|
173
|
+
*/
|
|
174
|
+
function getMarkdownTitle(lines, start_line_index) {
|
|
175
|
+
const first_line = lines[start_line_index];
|
|
176
|
+
|
|
177
|
+
if (first_line === undefined) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const trimmed_line = first_line.trim();
|
|
182
|
+
|
|
183
|
+
if (trimmed_line.length === 0) {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const heading_match = trimmed_line.match(HEADING_PATTERN);
|
|
188
|
+
|
|
189
|
+
if (heading_match) {
|
|
190
|
+
return {
|
|
191
|
+
line: start_line_index + 1,
|
|
192
|
+
value: heading_match[1].trim(),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
line: start_line_index + 1,
|
|
198
|
+
value: trimmed_line,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @param {string} line
|
|
204
|
+
* @returns {{ character: string, length: number } | null}
|
|
205
|
+
*/
|
|
206
|
+
function parseOpeningMarkdownFence(line) {
|
|
207
|
+
const trimmed_line = line.trimStart();
|
|
208
|
+
const fence_match = trimmed_line.match(MARKDOWN_FENCE_PATTERN);
|
|
209
|
+
|
|
210
|
+
if (!fence_match) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
character: fence_match[1][0],
|
|
216
|
+
length: fence_match[1].length,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* @param {string} line
|
|
222
|
+
* @param {{ character: string, length: number }} open_fence
|
|
223
|
+
* @returns {boolean}
|
|
224
|
+
*/
|
|
225
|
+
function isClosingMarkdownFence(line, open_fence) {
|
|
226
|
+
const trimmed_line = line.trimStart();
|
|
227
|
+
|
|
228
|
+
return trimmed_line.startsWith(
|
|
229
|
+
open_fence.character.repeat(open_fence.length),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* @param {string} file_path
|
|
235
|
+
* @param {PatramClaim[]} claims
|
|
236
|
+
* @param {PatramClaimFields} directive_fields
|
|
237
|
+
*/
|
|
238
|
+
function pushDirectiveClaim(file_path, claims, directive_fields) {
|
|
239
|
+
claims.push(
|
|
240
|
+
createClaim(file_path, claims.length + 1, 'directive', directive_fields),
|
|
241
|
+
);
|
|
242
|
+
}
|