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,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('./parse-cli-arguments.types.ts').CliCommandName} CliCommandName
|
|
3
|
+
* @typedef {import('./parse-cli-arguments.types.ts').CliOutputMode} CliOutputMode
|
|
4
|
+
* @typedef {{ kind: string, name?: string, rawName?: string, value?: string | boolean }} CliOptionToken
|
|
5
|
+
* @typedef {{ color?: string, json?: boolean, limit?: string, 'no-color'?: boolean, offset?: string, plain?: boolean, where?: string }} CliOptionValues
|
|
6
|
+
* @typedef {{ option_tokens: CliOptionToken[], positionals: string[], values: CliOptionValues }} ParsedCommandLine
|
|
7
|
+
* @typedef {{ allowed_option_names: Set<string>, extra_positionals_message: string, max_positionals: number, min_positionals: number, missing_positionals_message: string }} CommandSchema
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { findInvalidColorMode } from './parse-cli-color-options.js';
|
|
11
|
+
import { findInvalidQueryPagination } from './parse-cli-query-pagination.js';
|
|
12
|
+
|
|
13
|
+
const GLOBAL_OPTION_NAMES = new Set(['plain', 'json', 'color', 'no-color']);
|
|
14
|
+
export const CLI_OPTIONS = /** @type {const} */ ({
|
|
15
|
+
color: { type: 'string' },
|
|
16
|
+
json: { type: 'boolean' },
|
|
17
|
+
limit: { type: 'string' },
|
|
18
|
+
'no-color': { type: 'boolean' },
|
|
19
|
+
offset: { type: 'string' },
|
|
20
|
+
plain: { type: 'boolean' },
|
|
21
|
+
where: { type: 'string' },
|
|
22
|
+
});
|
|
23
|
+
/** @type {Record<CliCommandName, CommandSchema>} */
|
|
24
|
+
const COMMAND_SCHEMAS = {
|
|
25
|
+
check: createCommandSchema(0, 1, 'Check accepts at most one path.'),
|
|
26
|
+
queries: createCommandSchema(
|
|
27
|
+
0,
|
|
28
|
+
0,
|
|
29
|
+
'Queries does not accept positional arguments.',
|
|
30
|
+
),
|
|
31
|
+
query: {
|
|
32
|
+
...createCommandSchema(
|
|
33
|
+
0,
|
|
34
|
+
1,
|
|
35
|
+
'Query accepts either "--where" or a stored query name.',
|
|
36
|
+
),
|
|
37
|
+
allowed_option_names: new Set(['limit', 'offset', 'where']),
|
|
38
|
+
},
|
|
39
|
+
show: createCommandSchema(
|
|
40
|
+
1,
|
|
41
|
+
1,
|
|
42
|
+
'Show accepts exactly one file path.',
|
|
43
|
+
'Show requires a file path.',
|
|
44
|
+
),
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* @param {string | undefined} command_name
|
|
48
|
+
* @returns {command_name is CliCommandName}
|
|
49
|
+
*/
|
|
50
|
+
export function isCommandName(command_name) {
|
|
51
|
+
return (
|
|
52
|
+
command_name === 'check' ||
|
|
53
|
+
command_name === 'query' ||
|
|
54
|
+
command_name === 'queries' ||
|
|
55
|
+
command_name === 'show'
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* @param {CliOptionToken[]} tokens
|
|
60
|
+
* @returns {CliOptionToken[]}
|
|
61
|
+
*/
|
|
62
|
+
export function collectOptionTokens(tokens) {
|
|
63
|
+
return tokens.filter((token) => token.kind === 'option');
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* @param {CliCommandName} command_name
|
|
67
|
+
* @param {ParsedCommandLine} command_line
|
|
68
|
+
* @returns {string | null}
|
|
69
|
+
*/
|
|
70
|
+
export function validateParsedCommand(command_name, command_line) {
|
|
71
|
+
const command_positionals = command_line.positionals.slice(1);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
findUnknownOption(command_line.option_tokens) ??
|
|
75
|
+
findInvalidCommandOption(command_name, command_line.option_tokens) ??
|
|
76
|
+
findMissingOptionValue(command_line.option_tokens) ??
|
|
77
|
+
findOutputModeConflict(command_line.values) ??
|
|
78
|
+
findInvalidColorMode(command_line.option_tokens) ??
|
|
79
|
+
findInvalidQueryPagination(command_line.option_tokens) ??
|
|
80
|
+
findInvalidQueryMode(
|
|
81
|
+
command_name,
|
|
82
|
+
command_line.values,
|
|
83
|
+
command_positionals,
|
|
84
|
+
) ??
|
|
85
|
+
validateCommandPositionals(command_name, command_positionals)
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* @param {CliOptionValues} parsed_values
|
|
90
|
+
* @returns {CliOutputMode}
|
|
91
|
+
*/
|
|
92
|
+
export function resolveOutputMode(parsed_values) {
|
|
93
|
+
if (parsed_values.json) {
|
|
94
|
+
return 'json';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (parsed_values.plain) {
|
|
98
|
+
return 'plain';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return 'default';
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* @param {CliCommandName} command_name
|
|
105
|
+
* @param {string[]} command_positionals
|
|
106
|
+
* @param {CliOptionValues} parsed_values
|
|
107
|
+
* @returns {string[]}
|
|
108
|
+
*/
|
|
109
|
+
export function buildCommandArguments(
|
|
110
|
+
command_name,
|
|
111
|
+
command_positionals,
|
|
112
|
+
parsed_values,
|
|
113
|
+
) {
|
|
114
|
+
if (command_name === 'query' && parsed_values.where !== undefined) {
|
|
115
|
+
return ['--where', parsed_values.where];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return command_positionals;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @param {string} message
|
|
123
|
+
* @returns {{ message: string, success: false }}
|
|
124
|
+
*/
|
|
125
|
+
export function createParseError(message) {
|
|
126
|
+
return {
|
|
127
|
+
message,
|
|
128
|
+
success: false,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {CliOptionToken[]} option_tokens
|
|
134
|
+
* @returns {string | null}
|
|
135
|
+
*/
|
|
136
|
+
function findUnknownOption(option_tokens) {
|
|
137
|
+
for (const token of option_tokens) {
|
|
138
|
+
if (
|
|
139
|
+
token.name &&
|
|
140
|
+
token.rawName &&
|
|
141
|
+
!GLOBAL_OPTION_NAMES.has(token.name) &&
|
|
142
|
+
token.name !== 'limit' &&
|
|
143
|
+
token.name !== 'offset' &&
|
|
144
|
+
token.name !== 'where'
|
|
145
|
+
) {
|
|
146
|
+
return `Unknown option "${token.rawName}".`;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @param {CliCommandName} command_name
|
|
155
|
+
* @param {CliOptionToken[]} option_tokens
|
|
156
|
+
* @returns {string | null}
|
|
157
|
+
*/
|
|
158
|
+
function findInvalidCommandOption(command_name, option_tokens) {
|
|
159
|
+
const command_schema = COMMAND_SCHEMAS[command_name];
|
|
160
|
+
|
|
161
|
+
for (const token of option_tokens) {
|
|
162
|
+
if (!token.name || !token.rawName || GLOBAL_OPTION_NAMES.has(token.name)) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!command_schema.allowed_option_names.has(token.name)) {
|
|
167
|
+
return `Option "${token.rawName}" is not valid for "${command_name}".`;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @param {CliOptionToken[]} option_tokens
|
|
176
|
+
* @returns {string | null}
|
|
177
|
+
*/
|
|
178
|
+
function findMissingOptionValue(option_tokens) {
|
|
179
|
+
for (const token of option_tokens) {
|
|
180
|
+
if (token.name === 'where' && typeof token.value !== 'string') {
|
|
181
|
+
return 'Query requires a where clause.';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (token.name === 'offset' && typeof token.value !== 'string') {
|
|
185
|
+
return 'Offset requires a value.';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (token.name === 'limit' && typeof token.value !== 'string') {
|
|
189
|
+
return 'Limit requires a value.';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (token.name === 'color' && typeof token.value !== 'string') {
|
|
193
|
+
return 'Color requires a value.';
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* @param {CliOptionValues} parsed_values
|
|
202
|
+
* @returns {string | null}
|
|
203
|
+
*/
|
|
204
|
+
function findOutputModeConflict(parsed_values) {
|
|
205
|
+
if (parsed_values.plain && parsed_values.json) {
|
|
206
|
+
return 'Output mode accepts at most one of "--plain" or "--json".';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @param {CliCommandName} command_name
|
|
214
|
+
* @param {CliOptionValues} parsed_values
|
|
215
|
+
* @param {string[]} command_positionals
|
|
216
|
+
* @returns {string | null}
|
|
217
|
+
*/
|
|
218
|
+
function findInvalidQueryMode(
|
|
219
|
+
command_name,
|
|
220
|
+
parsed_values,
|
|
221
|
+
command_positionals,
|
|
222
|
+
) {
|
|
223
|
+
if (
|
|
224
|
+
command_name === 'query' &&
|
|
225
|
+
parsed_values.where !== undefined &&
|
|
226
|
+
command_positionals.length > 0
|
|
227
|
+
) {
|
|
228
|
+
return 'Query accepts either "--where" or a stored query name.';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @param {CliCommandName} command_name
|
|
236
|
+
* @param {string[]} command_positionals
|
|
237
|
+
* @returns {string | null}
|
|
238
|
+
*/
|
|
239
|
+
function validateCommandPositionals(command_name, command_positionals) {
|
|
240
|
+
const command_schema = COMMAND_SCHEMAS[command_name];
|
|
241
|
+
|
|
242
|
+
if (command_positionals.length < command_schema.min_positionals) {
|
|
243
|
+
return command_schema.missing_positionals_message;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (command_positionals.length > command_schema.max_positionals) {
|
|
247
|
+
return command_schema.extra_positionals_message;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* @param {number} min_positionals
|
|
255
|
+
* @param {number} max_positionals
|
|
256
|
+
* @param {string} extra_positionals_message
|
|
257
|
+
* @param {string} missing_positionals_message
|
|
258
|
+
* @returns {CommandSchema}
|
|
259
|
+
*/
|
|
260
|
+
function createCommandSchema(
|
|
261
|
+
min_positionals,
|
|
262
|
+
max_positionals,
|
|
263
|
+
extra_positionals_message,
|
|
264
|
+
missing_positionals_message = '',
|
|
265
|
+
) {
|
|
266
|
+
return {
|
|
267
|
+
allowed_option_names: new Set(),
|
|
268
|
+
extra_positionals_message,
|
|
269
|
+
max_positionals,
|
|
270
|
+
min_positionals,
|
|
271
|
+
missing_positionals_message,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('./parse-cli-arguments-helpers.js').CliOptionValues} CliOptionValues
|
|
3
|
+
* @typedef {import('./parse-cli-arguments-helpers.js').ParsedCommandLine} ParsedCommandLine
|
|
4
|
+
* @typedef {import('./parse-cli-arguments.types.ts').ParseCliArgumentsResult} ParseCliArgumentsResult
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { parseArgs } from 'node:util';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
CLI_OPTIONS,
|
|
11
|
+
buildCommandArguments,
|
|
12
|
+
collectOptionTokens,
|
|
13
|
+
createParseError,
|
|
14
|
+
isCommandName,
|
|
15
|
+
resolveOutputMode,
|
|
16
|
+
validateParsedCommand,
|
|
17
|
+
} from './parse-cli-arguments-helpers.js';
|
|
18
|
+
import { resolveColorMode } from './parse-cli-color-options.js';
|
|
19
|
+
import { buildQueryPagination } from './parse-cli-query-pagination.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* CLI argument parsing.
|
|
23
|
+
*
|
|
24
|
+
* Normalizes raw argv into one validated Patram command plus shared output and
|
|
25
|
+
* pagination options.
|
|
26
|
+
*
|
|
27
|
+
* Kind: cli
|
|
28
|
+
* Status: active
|
|
29
|
+
* Tracked in: ../docs/plans/v0/source-anchor-dogfooding.md
|
|
30
|
+
* Decided by: ../docs/decisions/cli-argument-parser.md
|
|
31
|
+
* @patram
|
|
32
|
+
* @see {@link ./patram-cli.js}
|
|
33
|
+
* @see {@link ../docs/decisions/cli-argument-parser.md}
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse the CLI arguments into one validated command result.
|
|
38
|
+
*
|
|
39
|
+
* @param {string[]} cli_arguments
|
|
40
|
+
* @returns {ParseCliArgumentsResult}
|
|
41
|
+
*/
|
|
42
|
+
export function parseCliArguments(cli_arguments) {
|
|
43
|
+
const command_line = parseCommandLine(cli_arguments);
|
|
44
|
+
|
|
45
|
+
if (!command_line.success) {
|
|
46
|
+
return command_line;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const command_name = command_line.value.positionals[0];
|
|
50
|
+
|
|
51
|
+
if (!isCommandName(command_name)) {
|
|
52
|
+
return createParseError('Unknown command.');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const validation_message = validateParsedCommand(
|
|
56
|
+
command_name,
|
|
57
|
+
command_line.value,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (validation_message) {
|
|
61
|
+
return createParseError(validation_message);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const command_positionals = command_line.value.positionals.slice(1);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
success: true,
|
|
68
|
+
value: {
|
|
69
|
+
color_mode: resolveColorMode(command_line.value.option_tokens),
|
|
70
|
+
command_arguments: buildCommandArguments(
|
|
71
|
+
command_name,
|
|
72
|
+
command_positionals,
|
|
73
|
+
command_line.value.values,
|
|
74
|
+
),
|
|
75
|
+
command_name,
|
|
76
|
+
output_mode: resolveOutputMode(command_line.value.values),
|
|
77
|
+
...buildQueryPagination(command_line.value.values),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {string[]} cli_arguments
|
|
84
|
+
* @returns {{ success: true, value: ParsedCommandLine } | { message: string, success: false }}
|
|
85
|
+
*/
|
|
86
|
+
function parseCommandLine(cli_arguments) {
|
|
87
|
+
try {
|
|
88
|
+
const parsed_arguments = parseArgs({
|
|
89
|
+
allowPositionals: true,
|
|
90
|
+
args: cli_arguments,
|
|
91
|
+
options: CLI_OPTIONS,
|
|
92
|
+
strict: false,
|
|
93
|
+
tokens: true,
|
|
94
|
+
});
|
|
95
|
+
const parsed_values = /** @type {CliOptionValues} */ (
|
|
96
|
+
parsed_arguments.values
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
success: true,
|
|
101
|
+
value: {
|
|
102
|
+
option_tokens: collectOptionTokens(parsed_arguments.tokens ?? []),
|
|
103
|
+
positionals: parsed_arguments.positionals,
|
|
104
|
+
values: parsed_values,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
} catch (error) {
|
|
108
|
+
if (error instanceof Error) {
|
|
109
|
+
return createParseError(error.message);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type CliCommandName = 'check' | 'query' | 'queries' | 'show';
|
|
2
|
+
|
|
3
|
+
export type CliOutputMode = 'default' | 'plain' | 'json';
|
|
4
|
+
|
|
5
|
+
export type CliColorMode = 'auto' | 'always' | 'never';
|
|
6
|
+
|
|
7
|
+
export interface ParsedCliArguments {
|
|
8
|
+
color_mode: CliColorMode;
|
|
9
|
+
command_arguments: string[];
|
|
10
|
+
command_name: CliCommandName;
|
|
11
|
+
output_mode: CliOutputMode;
|
|
12
|
+
query_limit?: number;
|
|
13
|
+
query_offset?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ParseCliArgumentsResult =
|
|
17
|
+
| {
|
|
18
|
+
success: true;
|
|
19
|
+
value: ParsedCliArguments;
|
|
20
|
+
}
|
|
21
|
+
| {
|
|
22
|
+
message: string;
|
|
23
|
+
success: false;
|
|
24
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('./parse-cli-arguments-helpers.js').CliOptionToken} CliOptionToken
|
|
3
|
+
* @typedef {import('./parse-cli-arguments.types.ts').CliColorMode} CliColorMode
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const VALID_COLOR_MODES = new Set(['auto', 'always', 'never']);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {CliOptionToken[]} option_tokens
|
|
10
|
+
* @returns {CliColorMode}
|
|
11
|
+
*/
|
|
12
|
+
export function resolveColorMode(option_tokens) {
|
|
13
|
+
let color_mode = 'auto';
|
|
14
|
+
|
|
15
|
+
for (const token of option_tokens) {
|
|
16
|
+
if (token.name === 'no-color') {
|
|
17
|
+
color_mode = 'never';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (token.name === 'color' && typeof token.value === 'string') {
|
|
21
|
+
color_mode = token.value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return /** @type {CliColorMode} */ (color_mode);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {CliOptionToken[]} option_tokens
|
|
30
|
+
* @returns {string | null}
|
|
31
|
+
*/
|
|
32
|
+
export function findInvalidColorMode(option_tokens) {
|
|
33
|
+
for (const token of option_tokens) {
|
|
34
|
+
if (
|
|
35
|
+
token.name === 'color' &&
|
|
36
|
+
typeof token.value === 'string' &&
|
|
37
|
+
!VALID_COLOR_MODES.has(token.value)
|
|
38
|
+
) {
|
|
39
|
+
return 'Color must be one of "auto", "always", or "never".';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('./parse-cli-arguments-helpers.js').CliOptionToken} CliOptionToken
|
|
3
|
+
* @typedef {import('./parse-cli-arguments-helpers.js').CliOptionValues} CliOptionValues
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {CliOptionValues} parsed_values
|
|
8
|
+
* @returns {{ query_limit?: number, query_offset?: number }}
|
|
9
|
+
*/
|
|
10
|
+
export function buildQueryPagination(parsed_values) {
|
|
11
|
+
/** @type {{ query_limit?: number, query_offset?: number }} */
|
|
12
|
+
const query_pagination = {};
|
|
13
|
+
|
|
14
|
+
if (parsed_values.limit !== undefined) {
|
|
15
|
+
query_pagination.query_limit = Number(parsed_values.limit);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (parsed_values.offset !== undefined) {
|
|
19
|
+
query_pagination.query_offset = Number(parsed_values.offset);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return query_pagination;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {CliOptionToken[]} option_tokens
|
|
27
|
+
* @returns {string | null}
|
|
28
|
+
*/
|
|
29
|
+
export function findInvalidQueryPagination(option_tokens) {
|
|
30
|
+
for (const token of option_tokens) {
|
|
31
|
+
if (
|
|
32
|
+
token.name === 'offset' &&
|
|
33
|
+
typeof token.value === 'string' &&
|
|
34
|
+
!/^\d+$/du.test(token.value)
|
|
35
|
+
) {
|
|
36
|
+
return 'Offset must be a non-negative integer.';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (
|
|
40
|
+
token.name === 'limit' &&
|
|
41
|
+
typeof token.value === 'string' &&
|
|
42
|
+
!/^\d+$/du.test(token.value)
|
|
43
|
+
) {
|
|
44
|
+
return 'Limit must be a non-negative integer.';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect JSDoc blocks and their activated `@patram` markers.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} source_text
|
|
5
|
+
* @returns {Array<{ activation_column: number | null, activation_line: number | null, lines: Array<{ column: number, content: string, line: number }> }>}
|
|
6
|
+
*/
|
|
7
|
+
export function collectJsdocBlocks(source_text) {
|
|
8
|
+
const source_lines = source_text.split('\n');
|
|
9
|
+
/** @type {Array<{ activation_column: number | null, activation_line: number | null, lines: Array<{ column: number, content: string, line: number }> }>} */
|
|
10
|
+
const jsdoc_blocks = [];
|
|
11
|
+
|
|
12
|
+
for (let line_index = 0; line_index < source_lines.length; line_index += 1) {
|
|
13
|
+
if (!source_lines[line_index].includes('/**')) {
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const closing_line_index = findJsdocClosingLineIndex(
|
|
18
|
+
source_lines,
|
|
19
|
+
line_index,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
if (closing_line_index < 0) {
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const block_lines = source_lines
|
|
27
|
+
.slice(line_index, closing_line_index + 1)
|
|
28
|
+
.map((raw_line, block_line_index) =>
|
|
29
|
+
createJsdocBlockLine(
|
|
30
|
+
raw_line,
|
|
31
|
+
line_index + block_line_index + 1,
|
|
32
|
+
block_line_index === 0,
|
|
33
|
+
line_index + block_line_index === closing_line_index,
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
const activation_line = block_lines.find((block_line) =>
|
|
37
|
+
/^@patram(?:\s|$)/du.test(block_line.content),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
jsdoc_blocks.push({
|
|
41
|
+
activation_column: activation_line?.column ?? null,
|
|
42
|
+
activation_line: activation_line?.line ?? null,
|
|
43
|
+
lines: block_lines,
|
|
44
|
+
});
|
|
45
|
+
line_index = closing_line_index;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return jsdoc_blocks;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {string[]} source_lines
|
|
53
|
+
* @param {number} start_line_index
|
|
54
|
+
* @returns {number}
|
|
55
|
+
*/
|
|
56
|
+
function findJsdocClosingLineIndex(source_lines, start_line_index) {
|
|
57
|
+
for (
|
|
58
|
+
let line_index = start_line_index;
|
|
59
|
+
line_index < source_lines.length;
|
|
60
|
+
line_index += 1
|
|
61
|
+
) {
|
|
62
|
+
const source_line = source_lines[line_index];
|
|
63
|
+
const search_start =
|
|
64
|
+
line_index === start_line_index ? source_line.indexOf('/**') + 3 : 0;
|
|
65
|
+
|
|
66
|
+
if (source_line.indexOf('*/', search_start) >= 0) {
|
|
67
|
+
return line_index;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return -1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {string} raw_line
|
|
76
|
+
* @param {number} line_number
|
|
77
|
+
* @param {boolean} is_first_line
|
|
78
|
+
* @param {boolean} is_last_line
|
|
79
|
+
* @returns {{ column: number, content: string, line: number }}
|
|
80
|
+
*/
|
|
81
|
+
function createJsdocBlockLine(
|
|
82
|
+
raw_line,
|
|
83
|
+
line_number,
|
|
84
|
+
is_first_line,
|
|
85
|
+
is_last_line,
|
|
86
|
+
) {
|
|
87
|
+
if (isLastClosingLine(raw_line, is_first_line, is_last_line)) {
|
|
88
|
+
return {
|
|
89
|
+
column: raw_line.indexOf('*/') + 1,
|
|
90
|
+
content: '',
|
|
91
|
+
line: line_number,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const line_parts = is_first_line
|
|
96
|
+
? extractFirstJsdocLineContent(raw_line, is_last_line)
|
|
97
|
+
: extractFollowingJsdocLineContent(raw_line, is_last_line);
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
column: line_parts.column,
|
|
101
|
+
content: line_parts.content.trim(),
|
|
102
|
+
line: line_number,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {string} raw_line
|
|
108
|
+
* @param {boolean} is_first_line
|
|
109
|
+
* @param {boolean} is_last_line
|
|
110
|
+
* @returns {boolean}
|
|
111
|
+
*/
|
|
112
|
+
function isLastClosingLine(raw_line, is_first_line, is_last_line) {
|
|
113
|
+
return is_last_line && !is_first_line && /^\s*\*\/\s*$/du.test(raw_line);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* @param {string} raw_line
|
|
118
|
+
* @param {boolean} is_last_line
|
|
119
|
+
* @returns {{ column: number, content: string }}
|
|
120
|
+
*/
|
|
121
|
+
function extractFirstJsdocLineContent(raw_line, is_last_line) {
|
|
122
|
+
const start_index = raw_line.indexOf('/**');
|
|
123
|
+
let line_content = raw_line.slice(start_index + 3);
|
|
124
|
+
let column = start_index + 4;
|
|
125
|
+
|
|
126
|
+
if (line_content.startsWith(' ')) {
|
|
127
|
+
line_content = line_content.slice(1);
|
|
128
|
+
column += 1;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return finalizeJsdocLineContent(line_content, column, is_last_line);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* @param {string} raw_line
|
|
136
|
+
* @param {boolean} is_last_line
|
|
137
|
+
* @returns {{ column: number, content: string }}
|
|
138
|
+
*/
|
|
139
|
+
function extractFollowingJsdocLineContent(raw_line, is_last_line) {
|
|
140
|
+
const prefix_match = raw_line.match(/^\s*\*\s?/du);
|
|
141
|
+
const prefix_length = prefix_match ? prefix_match[0].length : 0;
|
|
142
|
+
const line_content = raw_line.slice(prefix_length);
|
|
143
|
+
|
|
144
|
+
return finalizeJsdocLineContent(
|
|
145
|
+
line_content,
|
|
146
|
+
prefix_length + 1,
|
|
147
|
+
is_last_line,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {string} line_content
|
|
153
|
+
* @param {number} column
|
|
154
|
+
* @param {boolean} is_last_line
|
|
155
|
+
* @returns {{ column: number, content: string }}
|
|
156
|
+
*/
|
|
157
|
+
function finalizeJsdocLineContent(line_content, column, is_last_line) {
|
|
158
|
+
const trimmed_line_content = is_last_line
|
|
159
|
+
? removeJsdocClosingDelimiter(line_content)
|
|
160
|
+
: line_content;
|
|
161
|
+
const leading_whitespace_match = trimmed_line_content.match(/^\s*/du);
|
|
162
|
+
const leading_whitespace_length = leading_whitespace_match
|
|
163
|
+
? leading_whitespace_match[0].length
|
|
164
|
+
: 0;
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
column: column + leading_whitespace_length,
|
|
168
|
+
content: trimmed_line_content,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* @param {string} line_content
|
|
174
|
+
* @returns {string}
|
|
175
|
+
*/
|
|
176
|
+
function removeJsdocClosingDelimiter(line_content) {
|
|
177
|
+
const closing_index = line_content.indexOf('*/');
|
|
178
|
+
|
|
179
|
+
if (closing_index < 0) {
|
|
180
|
+
return line_content;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return line_content.slice(0, closing_index);
|
|
184
|
+
}
|