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.
Files changed (51) hide show
  1. package/bin/patram.js +25 -147
  2. package/lib/build-graph-identity.js +238 -0
  3. package/lib/build-graph.js +143 -77
  4. package/lib/check-graph.js +23 -7
  5. package/lib/claim-helpers.js +55 -0
  6. package/lib/command-output.js +83 -0
  7. package/lib/layout-stored-queries.js +213 -0
  8. package/lib/list-queries.js +18 -0
  9. package/lib/list-source-files.js +50 -15
  10. package/lib/load-patram-config.js +106 -18
  11. package/lib/load-patram-config.types.ts +9 -0
  12. package/lib/load-project-graph.js +124 -0
  13. package/lib/output-view.types.ts +73 -0
  14. package/lib/parse-claims.js +38 -158
  15. package/lib/parse-claims.types.ts +7 -0
  16. package/lib/parse-cli-arguments-helpers.js +273 -0
  17. package/lib/parse-cli-arguments.js +114 -0
  18. package/lib/parse-cli-arguments.types.ts +24 -0
  19. package/lib/parse-cli-color-options.js +44 -0
  20. package/lib/parse-cli-query-pagination.js +49 -0
  21. package/lib/parse-jsdoc-blocks.js +184 -0
  22. package/lib/parse-jsdoc-claims.js +280 -0
  23. package/lib/parse-jsdoc-prose.js +111 -0
  24. package/lib/parse-markdown-claims.js +242 -0
  25. package/lib/parse-markdown-directives.js +136 -0
  26. package/lib/parse-where-clause.js +312 -0
  27. package/lib/patram-cli.js +337 -0
  28. package/lib/patram-config.js +3 -1
  29. package/lib/patram-config.types.ts +2 -1
  30. package/lib/query-graph.js +256 -0
  31. package/lib/render-check-output.js +315 -0
  32. package/lib/render-json-output.js +108 -0
  33. package/lib/render-output-view.js +193 -0
  34. package/lib/render-plain-output.js +237 -0
  35. package/lib/render-rich-output.js +293 -0
  36. package/lib/render-rich-source.js +1333 -0
  37. package/lib/resolve-check-target.js +190 -0
  38. package/lib/resolve-output-mode.js +60 -0
  39. package/lib/resolve-patram-graph-config.js +88 -0
  40. package/lib/resolve-where-clause.js +51 -0
  41. package/lib/show-document.js +311 -0
  42. package/lib/source-file-defaults.js +28 -0
  43. package/lib/write-paged-output.js +87 -0
  44. package/package.json +21 -10
  45. package/bin/patram.test.js +0 -184
  46. package/lib/build-graph.test.js +0 -141
  47. package/lib/check-graph.test.js +0 -103
  48. package/lib/list-source-files.test.js +0 -101
  49. package/lib/load-patram-config.test.js +0 -211
  50. package/lib/parse-claims.test.js +0 -113
  51. 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
+ }