patram 0.0.2 → 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/bin/patram.js +25 -147
- package/lib/build-graph-identity.js +270 -0
- package/lib/build-graph.js +156 -77
- package/lib/check-graph.js +23 -7
- package/lib/claim-helpers.js +55 -0
- package/lib/cli-help-metadata.js +552 -0
- package/lib/command-output.js +83 -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 +361 -0
- package/lib/list-queries.js +18 -0
- package/lib/list-source-files.js +50 -15
- package/lib/load-patram-config.js +505 -18
- package/lib/load-patram-config.types.ts +40 -0
- package/lib/load-project-graph.js +124 -0
- package/lib/output-view.types.ts +88 -0
- package/lib/parse-claims.js +38 -158
- package/lib/parse-claims.types.ts +7 -0
- package/lib/parse-cli-arguments-helpers.js +446 -0
- package/lib/parse-cli-arguments.js +266 -0
- package/lib/parse-cli-arguments.types.ts +69 -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 +707 -0
- package/lib/parse-where-clause.types.ts +70 -0
- package/lib/patram-cli.js +464 -0
- package/lib/patram-config.js +3 -1
- package/lib/patram-config.types.ts +2 -1
- package/lib/patram.js +6 -0
- package/lib/query-graph.js +368 -0
- package/lib/query-inspection.js +523 -0
- package/lib/render-check-output.js +315 -0
- package/lib/render-cli-help.js +419 -0
- package/lib/render-json-output.js +161 -0
- package/lib/render-output-view.js +222 -0
- package/lib/render-plain-output.js +182 -0
- package/lib/render-rich-output.js +240 -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 +66 -0
- package/lib/show-document.js +311 -0
- package/lib/source-file-defaults.js +28 -0
- 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/lib/write-paged-output.js +87 -0
- package/package.json +28 -12
- 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,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { PatramDiagnostic } from './load-patram-config.types.ts';
|
|
3
|
+
*/
|
|
4
|
+
import { access, stat } from 'node:fs/promises';
|
|
5
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
|
|
8
|
+
const CONFIG_FILE_NAME = '.patram.json';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {(
|
|
12
|
+
* | { project_directory: string, target_kind: 'project' }
|
|
13
|
+
* | { project_directory: string, target_kind: 'directory', target_path: string }
|
|
14
|
+
* | { project_directory: string, target_kind: 'file', target_path: string }
|
|
15
|
+
* )} ResolvedCheckTarget
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the project directory and target scope for `check`.
|
|
20
|
+
*
|
|
21
|
+
* @param {string | undefined} target_argument
|
|
22
|
+
* @returns {Promise<ResolvedCheckTarget>}
|
|
23
|
+
*/
|
|
24
|
+
export async function resolveCheckTarget(target_argument) {
|
|
25
|
+
if (target_argument === undefined) {
|
|
26
|
+
return {
|
|
27
|
+
project_directory: process.cwd(),
|
|
28
|
+
target_kind: 'project',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const absolute_target_path = resolve(process.cwd(), target_argument);
|
|
33
|
+
const target_stats = await stat(absolute_target_path);
|
|
34
|
+
const target_directory = target_stats.isDirectory()
|
|
35
|
+
? absolute_target_path
|
|
36
|
+
: dirname(absolute_target_path);
|
|
37
|
+
const project_directory = await findProjectDirectory(target_directory);
|
|
38
|
+
|
|
39
|
+
if (target_stats.isFile()) {
|
|
40
|
+
const target_path = normalizeRepoRelativePath(
|
|
41
|
+
relative(project_directory, absolute_target_path),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
project_directory,
|
|
46
|
+
target_kind: 'file',
|
|
47
|
+
target_path,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const target_path = normalizeRepoRelativePath(
|
|
52
|
+
relative(project_directory, absolute_target_path),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if (target_path.length === 0) {
|
|
56
|
+
return {
|
|
57
|
+
project_directory,
|
|
58
|
+
target_kind: 'project',
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
project_directory,
|
|
64
|
+
target_kind: 'directory',
|
|
65
|
+
target_path,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Select the source files covered by one resolved `check` target.
|
|
71
|
+
*
|
|
72
|
+
* @param {string[]} source_file_paths
|
|
73
|
+
* @param {ResolvedCheckTarget} resolved_target
|
|
74
|
+
* @returns {string[]}
|
|
75
|
+
*/
|
|
76
|
+
export function selectCheckTargetSourceFiles(
|
|
77
|
+
source_file_paths,
|
|
78
|
+
resolved_target,
|
|
79
|
+
) {
|
|
80
|
+
if (resolved_target.target_kind === 'project') {
|
|
81
|
+
return source_file_paths;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (resolved_target.target_kind === 'file') {
|
|
85
|
+
return source_file_paths.filter(
|
|
86
|
+
(source_file_path) => source_file_path === resolved_target.target_path,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return source_file_paths.filter((source_file_path) =>
|
|
91
|
+
isPathInsideDirectory(source_file_path, resolved_target.target_path),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Filter diagnostics to one resolved `check` target.
|
|
97
|
+
*
|
|
98
|
+
* @param {PatramDiagnostic[]} diagnostics
|
|
99
|
+
* @param {ResolvedCheckTarget} resolved_target
|
|
100
|
+
* @returns {PatramDiagnostic[]}
|
|
101
|
+
*/
|
|
102
|
+
export function selectCheckTargetDiagnostics(diagnostics, resolved_target) {
|
|
103
|
+
if (resolved_target.target_kind === 'project') {
|
|
104
|
+
return diagnostics;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (resolved_target.target_kind === 'file') {
|
|
108
|
+
return diagnostics.filter(
|
|
109
|
+
(diagnostic) => diagnostic.path === resolved_target.target_path,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return diagnostics.filter((diagnostic) =>
|
|
114
|
+
isPathInsideDirectory(diagnostic.path, resolved_target.target_path),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {string} start_directory
|
|
120
|
+
* @returns {Promise<string>}
|
|
121
|
+
*/
|
|
122
|
+
async function findProjectDirectory(start_directory) {
|
|
123
|
+
let current_directory = start_directory;
|
|
124
|
+
|
|
125
|
+
while (true) {
|
|
126
|
+
if (await hasConfigFile(current_directory)) {
|
|
127
|
+
return current_directory;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const parent_directory = dirname(current_directory);
|
|
131
|
+
|
|
132
|
+
if (parent_directory === current_directory) {
|
|
133
|
+
return start_directory;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
current_directory = parent_directory;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* @param {string} directory_path
|
|
142
|
+
* @returns {Promise<boolean>}
|
|
143
|
+
*/
|
|
144
|
+
async function hasConfigFile(directory_path) {
|
|
145
|
+
try {
|
|
146
|
+
await access(resolve(directory_path, CONFIG_FILE_NAME));
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (isMissingPathError(error)) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @param {string} source_path
|
|
160
|
+
* @param {string} directory_path
|
|
161
|
+
* @returns {boolean}
|
|
162
|
+
*/
|
|
163
|
+
function isPathInsideDirectory(source_path, directory_path) {
|
|
164
|
+
return (
|
|
165
|
+
source_path === directory_path ||
|
|
166
|
+
source_path.startsWith(`${directory_path}/`)
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* @param {string} source_path
|
|
172
|
+
* @returns {string}
|
|
173
|
+
*/
|
|
174
|
+
function normalizeRepoRelativePath(source_path) {
|
|
175
|
+
return source_path.replaceAll('\\', '/');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* @param {unknown} error
|
|
180
|
+
* @returns {error is NodeJS.ErrnoException}
|
|
181
|
+
*/
|
|
182
|
+
function isMissingPathError(error) {
|
|
183
|
+
if (!(error instanceof Error)) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
'code' in error && (error.code === 'ENOENT' || error.code === 'ENOTDIR')
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { ParsedCliArguments } from './parse-cli-arguments.types.ts';
|
|
3
|
+
* @import { ResolvedOutputMode } from './output-view.types.ts';
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the renderer and color support for one command invocation.
|
|
8
|
+
*
|
|
9
|
+
* @param {ParsedCliArguments} parsed_arguments
|
|
10
|
+
* @param {{ is_tty: boolean, no_color: boolean, term: string | undefined }} output_context
|
|
11
|
+
* @returns {ResolvedOutputMode}
|
|
12
|
+
*/
|
|
13
|
+
export function resolveOutputMode(parsed_arguments, output_context) {
|
|
14
|
+
if (parsed_arguments.output_mode === 'json') {
|
|
15
|
+
return {
|
|
16
|
+
color_enabled: false,
|
|
17
|
+
renderer_name: 'json',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (parsed_arguments.output_mode === 'plain') {
|
|
22
|
+
return {
|
|
23
|
+
color_enabled: false,
|
|
24
|
+
renderer_name: 'plain',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!output_context.is_tty) {
|
|
29
|
+
return {
|
|
30
|
+
color_enabled: false,
|
|
31
|
+
renderer_name: 'plain',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
color_enabled: isColorEnabled(parsed_arguments, output_context),
|
|
37
|
+
renderer_name: 'rich',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {ParsedCliArguments} parsed_arguments
|
|
43
|
+
* @param {{ is_tty: boolean, no_color: boolean, term: string | undefined }} output_context
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
function isColorEnabled(parsed_arguments, output_context) {
|
|
47
|
+
if (parsed_arguments.color_mode === 'always') {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (parsed_arguments.color_mode === 'never') {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (output_context.no_color || output_context.term === 'dumb') {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { PatramRepoConfig } from './load-patram-config.types.ts';
|
|
3
|
+
* @import { PatramConfig } from './patram-config.types.ts';
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { parsePatramConfig } from './patram-config.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Built-in graph semantics.
|
|
10
|
+
*
|
|
11
|
+
* Merges repo-defined graph config with Patram's built-in document and link
|
|
12
|
+
* semantics before graph materialization.
|
|
13
|
+
*
|
|
14
|
+
* Kind: config
|
|
15
|
+
* Status: active
|
|
16
|
+
* Tracked in: ../docs/plans/v0/source-anchor-dogfooding.md
|
|
17
|
+
* Decided by: ../docs/decisions/graph-materialization.md
|
|
18
|
+
* @patram
|
|
19
|
+
* @see {@link ./load-patram-config.js}
|
|
20
|
+
* @see {@link ../docs/graph-v0.md}
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const BUILT_IN_PATRAM_CONFIG = {
|
|
24
|
+
kinds: {
|
|
25
|
+
document: {
|
|
26
|
+
builtin: true,
|
|
27
|
+
label: 'Document',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
mappings: {
|
|
31
|
+
'document.title': {
|
|
32
|
+
node: {
|
|
33
|
+
field: 'title',
|
|
34
|
+
kind: 'document',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
'document.description': {
|
|
38
|
+
node: {
|
|
39
|
+
field: 'description',
|
|
40
|
+
kind: 'document',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
'jsdoc.link': {
|
|
44
|
+
emit: {
|
|
45
|
+
relation: 'links_to',
|
|
46
|
+
target: 'path',
|
|
47
|
+
target_kind: 'document',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
'markdown.link': {
|
|
51
|
+
emit: {
|
|
52
|
+
relation: 'links_to',
|
|
53
|
+
target: 'path',
|
|
54
|
+
target_kind: 'document',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
relations: {
|
|
59
|
+
links_to: {
|
|
60
|
+
builtin: true,
|
|
61
|
+
from: ['document'],
|
|
62
|
+
to: ['document'],
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Merge built-in Patram graph semantics with repo-defined schema.
|
|
69
|
+
*
|
|
70
|
+
* @param {PatramRepoConfig} repo_config
|
|
71
|
+
* @returns {PatramConfig}
|
|
72
|
+
*/
|
|
73
|
+
export function resolvePatramGraphConfig(repo_config) {
|
|
74
|
+
return parsePatramConfig({
|
|
75
|
+
kinds: {
|
|
76
|
+
...BUILT_IN_PATRAM_CONFIG.kinds,
|
|
77
|
+
...repo_config.kinds,
|
|
78
|
+
},
|
|
79
|
+
mappings: {
|
|
80
|
+
...BUILT_IN_PATRAM_CONFIG.mappings,
|
|
81
|
+
...repo_config.mappings,
|
|
82
|
+
},
|
|
83
|
+
relations: {
|
|
84
|
+
...BUILT_IN_PATRAM_CONFIG.relations,
|
|
85
|
+
...repo_config.relations,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { PatramRepoConfig } from './load-patram-config.types.ts';
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {{ kind: 'ad_hoc' } | { kind: 'stored_query', name: string }} QuerySource
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve an ad hoc or stored query into a where clause.
|
|
11
|
+
*
|
|
12
|
+
* @param {PatramRepoConfig} repo_config
|
|
13
|
+
* @param {string[]} command_arguments
|
|
14
|
+
* @returns {{ success: true, value: { query_source: QuerySource, where_clause: string } } | { success: false, message: string }}
|
|
15
|
+
*/
|
|
16
|
+
export function resolveWhereClause(repo_config, command_arguments) {
|
|
17
|
+
if (command_arguments[0] === '--where') {
|
|
18
|
+
const where_clause = command_arguments.slice(1).join(' ').trim();
|
|
19
|
+
|
|
20
|
+
if (where_clause.length === 0) {
|
|
21
|
+
return {
|
|
22
|
+
message: 'Query requires a where clause.',
|
|
23
|
+
success: false,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
success: true,
|
|
29
|
+
value: {
|
|
30
|
+
query_source: {
|
|
31
|
+
kind: 'ad_hoc',
|
|
32
|
+
},
|
|
33
|
+
where_clause,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const stored_query_name = command_arguments[0];
|
|
39
|
+
|
|
40
|
+
if (!stored_query_name) {
|
|
41
|
+
return {
|
|
42
|
+
message: 'Query requires "--where" or a stored query name.',
|
|
43
|
+
success: false,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const stored_query = repo_config.queries[stored_query_name];
|
|
48
|
+
|
|
49
|
+
if (!stored_query) {
|
|
50
|
+
return {
|
|
51
|
+
message: `Stored query "${stored_query_name}" was not found.`,
|
|
52
|
+
success: false,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
success: true,
|
|
58
|
+
value: {
|
|
59
|
+
query_source: {
|
|
60
|
+
kind: 'stored_query',
|
|
61
|
+
name: stored_query_name,
|
|
62
|
+
},
|
|
63
|
+
where_clause: stored_query.where,
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
2
|
+
/**
|
|
3
|
+
* @import { GraphNode } from './build-graph.types.ts';
|
|
4
|
+
* @import { PatramClaim } from './parse-claims.types.ts';
|
|
5
|
+
* @import { PatramDiagnostic } from './load-patram-config.types.ts';
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { posix, relative, resolve } from 'node:path';
|
|
10
|
+
|
|
11
|
+
import { parseSourceFile } from './parse-claims.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Show command document rendering.
|
|
15
|
+
*
|
|
16
|
+
* Loads one source file, resolves indexed links, and builds the shared show
|
|
17
|
+
* output model.
|
|
18
|
+
*
|
|
19
|
+
* Kind: output
|
|
20
|
+
* Status: active
|
|
21
|
+
* Tracked in: ../docs/plans/v0/source-anchor-dogfooding.md
|
|
22
|
+
* Decided by: ../docs/decisions/show-output.md
|
|
23
|
+
* Decided by: ../docs/decisions/source-rendering.md
|
|
24
|
+
* @patram
|
|
25
|
+
* @see {@link ./render-output-view.js}
|
|
26
|
+
* @see {@link ../docs/decisions/show-output.md}
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {string} requested_file_path
|
|
31
|
+
* @param {string} project_directory
|
|
32
|
+
* @param {import('./build-graph.types.ts').BuildGraphResult} graph
|
|
33
|
+
* @returns {Promise<
|
|
34
|
+
* | {
|
|
35
|
+
* success: true;
|
|
36
|
+
* value: {
|
|
37
|
+
* path: string;
|
|
38
|
+
* rendered_source: string;
|
|
39
|
+
* resolved_links: Array<{
|
|
40
|
+
* label: string;
|
|
41
|
+
* reference: number;
|
|
42
|
+
* target: { kind?: string, path: string, status?: string, title: string };
|
|
43
|
+
* }>;
|
|
44
|
+
* source: string;
|
|
45
|
+
* };
|
|
46
|
+
* }
|
|
47
|
+
* | {
|
|
48
|
+
* diagnostic: PatramDiagnostic;
|
|
49
|
+
* success: false;
|
|
50
|
+
* }
|
|
51
|
+
* >}
|
|
52
|
+
*/
|
|
53
|
+
export async function loadShowOutput(
|
|
54
|
+
requested_file_path,
|
|
55
|
+
project_directory,
|
|
56
|
+
graph,
|
|
57
|
+
) {
|
|
58
|
+
const absolute_source_path = resolve(project_directory, requested_file_path);
|
|
59
|
+
const source_file_path = normalizeRepoRelativePath(
|
|
60
|
+
relative(project_directory, absolute_source_path),
|
|
61
|
+
);
|
|
62
|
+
let source_text;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
source_text = await readFile(absolute_source_path, 'utf8');
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (isFileNotFoundError(error)) {
|
|
68
|
+
return {
|
|
69
|
+
diagnostic: createShowFileNotFoundDiagnostic(source_file_path),
|
|
70
|
+
success: false,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const parse_result = parseSourceFile({
|
|
78
|
+
path: source_file_path,
|
|
79
|
+
source: source_text,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (parse_result.diagnostics.length > 0) {
|
|
83
|
+
return {
|
|
84
|
+
diagnostic: parse_result.diagnostics[0],
|
|
85
|
+
success: false,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
success: true,
|
|
91
|
+
value: createShowOutput(
|
|
92
|
+
source_file_path,
|
|
93
|
+
source_text,
|
|
94
|
+
parse_result.claims,
|
|
95
|
+
graph.nodes,
|
|
96
|
+
),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {string} source_file_path
|
|
102
|
+
* @param {string} source_text
|
|
103
|
+
* @param {PatramClaim[]} claims
|
|
104
|
+
* @param {Record<string, GraphNode>} graph_nodes
|
|
105
|
+
* @returns {{ path: string, rendered_source: string, resolved_links: Array<{ label: string, reference: number, target: { kind?: string, path: string, status?: string, title: string } }>, source: string }}
|
|
106
|
+
*/
|
|
107
|
+
function createShowOutput(source_file_path, source_text, claims, graph_nodes) {
|
|
108
|
+
const link_claims = claims.filter(isResolvedLinkClaim);
|
|
109
|
+
const rendered_link_claims = link_claims.filter(isMarkdownLinkClaim);
|
|
110
|
+
const resolved_links = link_claims.map((claim, claim_index) =>
|
|
111
|
+
createResolvedLinkSummary(
|
|
112
|
+
source_file_path,
|
|
113
|
+
claim,
|
|
114
|
+
claim_index + 1,
|
|
115
|
+
graph_nodes,
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
path: source_file_path,
|
|
121
|
+
rendered_source: renderResolvedSource(
|
|
122
|
+
source_text,
|
|
123
|
+
rendered_link_claims,
|
|
124
|
+
resolved_links,
|
|
125
|
+
),
|
|
126
|
+
resolved_links,
|
|
127
|
+
source: source_text,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @param {string} source_text
|
|
133
|
+
* @param {PatramClaim[]} link_claims
|
|
134
|
+
* @param {Array<{ label: string, reference: number }>} resolved_links
|
|
135
|
+
* @returns {string}
|
|
136
|
+
*/
|
|
137
|
+
function renderResolvedSource(source_text, link_claims, resolved_links) {
|
|
138
|
+
const source_lines = source_text.split('\n');
|
|
139
|
+
|
|
140
|
+
return trimTrailingLineBreaks(
|
|
141
|
+
source_lines
|
|
142
|
+
.map((source_line, line_index) =>
|
|
143
|
+
renderResolvedSourceLine(
|
|
144
|
+
source_line,
|
|
145
|
+
line_index + 1,
|
|
146
|
+
link_claims,
|
|
147
|
+
resolved_links,
|
|
148
|
+
),
|
|
149
|
+
)
|
|
150
|
+
.join('\n'),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {string} source_line
|
|
156
|
+
* @param {number} line_number
|
|
157
|
+
* @param {PatramClaim[]} link_claims
|
|
158
|
+
* @param {Array<{ label: string, reference: number }>} resolved_links
|
|
159
|
+
* @returns {string}
|
|
160
|
+
*/
|
|
161
|
+
function renderResolvedSourceLine(
|
|
162
|
+
source_line,
|
|
163
|
+
line_number,
|
|
164
|
+
link_claims,
|
|
165
|
+
resolved_links,
|
|
166
|
+
) {
|
|
167
|
+
const line_link_claims = link_claims.filter(
|
|
168
|
+
(claim) => claim.origin.line === line_number,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
if (line_link_claims.length === 0) {
|
|
172
|
+
return source_line;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** @type {string[]} */
|
|
176
|
+
const chunks = [];
|
|
177
|
+
let source_offset = 0;
|
|
178
|
+
|
|
179
|
+
for (const claim of line_link_claims) {
|
|
180
|
+
const claim_index = link_claims.indexOf(claim);
|
|
181
|
+
const claim_value = getLinkClaimValue(claim);
|
|
182
|
+
const rendered_link = resolved_links[claim_index];
|
|
183
|
+
const claim_column = claim.origin.column - 1;
|
|
184
|
+
const raw_link = `[${claim_value.text}](${claim_value.target})`;
|
|
185
|
+
|
|
186
|
+
chunks.push(source_line.slice(source_offset, claim_column));
|
|
187
|
+
chunks.push(`[${rendered_link.label}][${rendered_link.reference}]`);
|
|
188
|
+
source_offset = claim_column + raw_link.length;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
chunks.push(source_line.slice(source_offset));
|
|
192
|
+
|
|
193
|
+
return chunks.join('');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @param {string} source_file_path
|
|
198
|
+
* @param {PatramClaim} claim
|
|
199
|
+
* @param {number} reference
|
|
200
|
+
* @param {Record<string, GraphNode>} graph_nodes
|
|
201
|
+
* @returns {{ label: string, reference: number, target: { kind?: string, path: string, status?: string, title: string } }}
|
|
202
|
+
*/
|
|
203
|
+
function createResolvedLinkSummary(
|
|
204
|
+
source_file_path,
|
|
205
|
+
claim,
|
|
206
|
+
reference,
|
|
207
|
+
graph_nodes,
|
|
208
|
+
) {
|
|
209
|
+
const claim_value = getLinkClaimValue(claim);
|
|
210
|
+
const target_path = resolveShowTargetPath(
|
|
211
|
+
source_file_path,
|
|
212
|
+
claim_value.target,
|
|
213
|
+
);
|
|
214
|
+
const target_node = graph_nodes[`doc:${target_path}`];
|
|
215
|
+
const target_title = target_node?.title ?? claim_value.text;
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
label: claim_value.text,
|
|
219
|
+
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
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @param {string} source_file_path
|
|
231
|
+
* @param {string} raw_target
|
|
232
|
+
* @returns {string}
|
|
233
|
+
*/
|
|
234
|
+
function resolveShowTargetPath(source_file_path, raw_target) {
|
|
235
|
+
const source_directory = posix.dirname(
|
|
236
|
+
normalizeRepoRelativePath(source_file_path),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
return normalizeRepoRelativePath(posix.join(source_directory, raw_target));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* @param {PatramClaim} claim
|
|
244
|
+
* @returns {claim is PatramClaim & { type: 'markdown.link', value: { target: string, text: string } }}
|
|
245
|
+
*/
|
|
246
|
+
function isMarkdownLinkClaim(claim) {
|
|
247
|
+
return claim.type === 'markdown.link';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* @param {PatramClaim} claim
|
|
252
|
+
* @returns {claim is PatramClaim & { type: 'jsdoc.link' | 'markdown.link', value: { target: string, text: string } }}
|
|
253
|
+
*/
|
|
254
|
+
function isResolvedLinkClaim(claim) {
|
|
255
|
+
return claim.type === 'markdown.link' || claim.type === 'jsdoc.link';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* @param {PatramClaim} claim
|
|
260
|
+
* @returns {{ target: string, text: string }}
|
|
261
|
+
*/
|
|
262
|
+
function getLinkClaimValue(claim) {
|
|
263
|
+
if (typeof claim.value === 'string') {
|
|
264
|
+
throw new Error(`Expected claim "${claim.id}" to carry a markdown link.`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return claim.value;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* @param {string} source_path
|
|
272
|
+
* @returns {string}
|
|
273
|
+
*/
|
|
274
|
+
function normalizeRepoRelativePath(source_path) {
|
|
275
|
+
return posix.normalize(source_path.replaceAll('\\', '/'));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* @param {string} source_file_path
|
|
280
|
+
* @returns {PatramDiagnostic}
|
|
281
|
+
*/
|
|
282
|
+
function createShowFileNotFoundDiagnostic(source_file_path) {
|
|
283
|
+
return {
|
|
284
|
+
code: 'show.file_not_found',
|
|
285
|
+
column: 1,
|
|
286
|
+
level: 'error',
|
|
287
|
+
line: 1,
|
|
288
|
+
message: `File "${source_file_path}" was not found.`,
|
|
289
|
+
path: source_file_path,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* @param {string} value
|
|
295
|
+
* @returns {string}
|
|
296
|
+
*/
|
|
297
|
+
function trimTrailingLineBreaks(value) {
|
|
298
|
+
return value.replace(/\n+$/du, '');
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* @param {unknown} error
|
|
303
|
+
* @returns {error is NodeJS.ErrnoException}
|
|
304
|
+
*/
|
|
305
|
+
function isFileNotFoundError(error) {
|
|
306
|
+
if (!(error instanceof Error)) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return 'code' in error && error.code === 'ENOENT';
|
|
311
|
+
}
|