patram 0.11.0 → 0.12.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.
Files changed (110) hide show
  1. package/bin/patram.js +4 -4
  2. package/lib/cli/commands/fields.js +0 -4
  3. package/lib/cli/commands/queries.js +10 -20
  4. package/lib/cli/commands/query.js +1 -8
  5. package/lib/cli/commands/refs.js +3 -10
  6. package/lib/cli/commands/show.js +1 -8
  7. package/lib/cli/help-metadata.js +71 -106
  8. package/lib/cli/main.js +10 -10
  9. package/lib/cli/parse-arguments-helpers.js +165 -59
  10. package/lib/cli/parse-arguments.js +4 -4
  11. package/lib/cli/render-help.js +2 -2
  12. package/lib/config/defaults.js +33 -25
  13. package/lib/config/load-patram-config.d.ts +8 -33
  14. package/lib/config/load-patram-config.js +9 -33
  15. package/lib/config/load-patram-config.types.d.ts +3 -40
  16. package/lib/config/manage-stored-queries-helpers.d.ts +4 -4
  17. package/lib/config/manage-stored-queries-helpers.js +91 -33
  18. package/lib/config/manage-stored-queries.d.ts +4 -4
  19. package/lib/config/manage-stored-queries.js +11 -5
  20. package/lib/config/patram-config.d.ts +34 -34
  21. package/lib/config/patram-config.js +3 -3
  22. package/lib/config/patram-config.types.d.ts +5 -11
  23. package/lib/config/resolve-patram-graph-config.d.ts +5 -1
  24. package/lib/config/resolve-patram-graph-config.js +3 -119
  25. package/lib/config/schema.d.ts +158 -269
  26. package/lib/config/schema.js +72 -210
  27. package/lib/config/validate-patram-config-value.js +6 -31
  28. package/lib/config/validation.d.ts +2 -12
  29. package/lib/config/validation.js +125 -483
  30. package/lib/find-close-match.d.ts +4 -1
  31. package/lib/graph/build-graph-identity.d.ts +1 -32
  32. package/lib/graph/build-graph-identity.js +5 -269
  33. package/lib/graph/build-graph.d.ts +13 -4
  34. package/lib/graph/build-graph.js +347 -488
  35. package/lib/graph/build-graph.types.d.ts +8 -9
  36. package/lib/graph/check-directive-metadata-helpers.d.ts +30 -0
  37. package/lib/graph/check-directive-metadata-helpers.js +126 -0
  38. package/lib/graph/check-directive-metadata.d.ts +8 -9
  39. package/lib/graph/check-directive-metadata.js +70 -561
  40. package/lib/graph/check-directive-path-target.d.ts +6 -13
  41. package/lib/graph/check-directive-path-target.js +26 -57
  42. package/lib/graph/check-directive-value.d.ts +1 -5
  43. package/lib/graph/check-directive-value.js +40 -180
  44. package/lib/graph/check-graph.d.ts +5 -5
  45. package/lib/graph/check-graph.js +8 -6
  46. package/lib/graph/document-node-identity.d.ts +23 -7
  47. package/lib/graph/document-node-identity.js +417 -160
  48. package/lib/graph/graph-node.d.ts +42 -0
  49. package/lib/graph/graph-node.js +83 -0
  50. package/lib/graph/inspect-reverse-references.js +16 -11
  51. package/lib/graph/load-project-graph.d.ts +7 -7
  52. package/lib/graph/load-project-graph.js +7 -7
  53. package/lib/graph/parse-where-clause.types.d.ts +3 -2
  54. package/lib/graph/query/cypher-reader.d.ts +59 -0
  55. package/lib/graph/query/cypher-reader.js +151 -0
  56. package/lib/graph/query/cypher-support.d.ts +79 -0
  57. package/lib/graph/query/cypher-support.js +213 -0
  58. package/lib/graph/query/cypher-tokenize.d.ts +13 -0
  59. package/lib/graph/query/cypher-tokenize.js +225 -0
  60. package/lib/graph/query/cypher.types.d.ts +43 -0
  61. package/lib/graph/query/execute.d.ts +7 -7
  62. package/lib/graph/query/execute.js +71 -33
  63. package/lib/graph/query/inspect.js +58 -24
  64. package/lib/graph/query/parse-cypher-patterns.d.ts +27 -0
  65. package/lib/graph/query/parse-cypher-patterns.js +382 -0
  66. package/lib/graph/query/parse-cypher.d.ts +7 -0
  67. package/lib/graph/query/parse-cypher.js +580 -0
  68. package/lib/graph/query/parse-query.d.ts +13 -0
  69. package/lib/graph/query/parse-query.js +97 -0
  70. package/lib/graph/query/resolve.js +77 -23
  71. package/lib/output/command-output.js +12 -5
  72. package/lib/output/compact-layout.js +221 -0
  73. package/lib/output/format-output-item-block.js +31 -1
  74. package/lib/output/format-output-metadata.js +16 -29
  75. package/lib/output/format-stored-query-block.js +95 -0
  76. package/lib/output/layout-incoming-references.js +101 -19
  77. package/lib/output/layout-stored-queries.js +23 -330
  78. package/lib/output/list-queries.js +1 -1
  79. package/lib/output/render-field-discovery.js +11 -2
  80. package/lib/output/render-output-view.js +9 -5
  81. package/lib/output/renderers/json.js +5 -26
  82. package/lib/output/renderers/plain.js +155 -35
  83. package/lib/output/renderers/rich.js +250 -36
  84. package/lib/output/resolved-link-layout.js +43 -0
  85. package/lib/output/rich-source/render.js +193 -35
  86. package/lib/output/show-document.js +25 -18
  87. package/lib/output/view-model/index.js +124 -103
  88. package/lib/parse/jsdoc/parse-jsdoc-blocks.js +1 -1
  89. package/lib/parse/jsdoc/parse-jsdoc-claims.js +12 -6
  90. package/lib/parse/markdown/parse-markdown-claims.js +99 -62
  91. package/lib/parse/markdown/parse-markdown-directives.d.ts +10 -6
  92. package/lib/parse/markdown/parse-markdown-directives.js +104 -18
  93. package/lib/parse/markdown/parse-markdown-prose.d.ts +27 -0
  94. package/lib/parse/markdown/parse-markdown-prose.js +243 -0
  95. package/lib/parse/parse-claims.d.ts +2 -6
  96. package/lib/parse/parse-claims.js +11 -53
  97. package/lib/parse/tagged-fenced/tagged-fenced-blocks.d.ts +4 -4
  98. package/lib/parse/tagged-fenced/tagged-fenced-blocks.js +4 -4
  99. package/lib/parse/yaml/parse-yaml-claims.js +4 -4
  100. package/lib/patram.d.ts +3 -5
  101. package/lib/patram.js +1 -1
  102. package/lib/scan/discover-fields.js +194 -55
  103. package/lib/scan/list-source-files.d.ts +4 -4
  104. package/lib/scan/list-source-files.js +4 -4
  105. package/package.json +1 -1
  106. package/lib/directive-validation-test-helpers.js +0 -87
  107. package/lib/graph/query/parse.d.ts +0 -75
  108. package/lib/graph/query/parse.js +0 -1064
  109. package/lib/output/derived-summary.js +0 -280
  110. package/lib/output/format-derived-summary-row.js +0 -9
@@ -5,8 +5,12 @@
5
5
 
6
6
  import { createClaim, isPathLikeTarget } from '../claim-helpers.js';
7
7
  import {
8
+ getMarkdownDescription,
9
+ getMarkdownTitle,
10
+ } from './parse-markdown-prose.js';
11
+ import {
12
+ collectVisibleDirectiveFields,
8
13
  matchHiddenDirectiveFields,
9
- matchVisibleDirectiveFields,
10
14
  parseFrontMatterDirectiveFields,
11
15
  } from './parse-markdown-directives.js';
12
16
 
@@ -16,17 +20,16 @@ import {
16
20
  * Extracts document titles, directives, and links from markdown source while
17
21
  * ignoring fenced-code link noise.
18
22
  *
19
- * Kind: parse
20
- * Status: active
21
- * Tracked in: ../../../docs/plans/v0/source-anchor-dogfooding.md
22
- * Decided by: ../../../docs/decisions/markdown-metadata-directive-syntax.md
23
- * Decided by: ../../../docs/decisions/markdown-link-claim-scope.md
23
+ * kind: parse
24
+ * status: active
25
+ * tracked_in: ../../../docs/plans/v0/source-anchor-dogfooding.md
26
+ * decided_by: ../../../docs/decisions/markdown-metadata-directive-syntax.md
27
+ * decided_by: ../../../docs/decisions/markdown-link-claim-scope.md
24
28
  * @patram
25
29
  * @see {@link ../parse-claims.js}
26
30
  * @see {@link ../../../docs/decisions/markdown-metadata-directive-syntax.md}
27
31
  */
28
32
 
29
- const HEADING_PATTERN = /^#\s+(.+)$/du;
30
33
  const MARKDOWN_FENCE_PATTERN = /^([`~]{3,})/du;
31
34
  const MARKDOWN_LINK_PATTERN = /\[([^\]]+)\]\(([^)]+)\)/dgu;
32
35
 
@@ -37,30 +40,19 @@ const MARKDOWN_LINK_PATTERN = /\[([^\]]+)\]\(([^)]+)\)/dgu;
37
40
  */
38
41
  export function parseMarkdownClaims(parse_input, parse_options) {
39
42
  const lines = parse_input.source.split('\n');
40
-
41
- /** @type {PatramClaim[]} */
42
- const claims = [];
43
43
  const front_matter_result = parseFrontMatterDirectiveFields(
44
44
  parse_input.path,
45
45
  lines,
46
46
  parse_options,
47
47
  );
48
+ /** @type {PatramClaim[]} */
49
+ const claims = collectMarkdownMetadataClaims(
50
+ parse_input.path,
51
+ lines,
52
+ front_matter_result.body_start,
53
+ );
48
54
  /** @type {{ character: string, length: number } | null} */
49
55
  let open_fence = null;
50
- const title_result = getMarkdownTitle(lines, front_matter_result.body_start);
51
-
52
- if (title_result) {
53
- claims.push(
54
- createClaim(parse_input.path, claims.length + 1, 'document.title', {
55
- origin: {
56
- column: 1,
57
- line: title_result.line,
58
- path: parse_input.path,
59
- },
60
- value: title_result.value,
61
- }),
62
- );
63
- }
64
56
 
65
57
  for (const directive_fields of front_matter_result.directive_fields) {
66
58
  claims.push(
@@ -70,11 +62,12 @@ export function parseMarkdownClaims(parse_input, parse_options) {
70
62
  );
71
63
  }
72
64
 
73
- for (const [line_index, line] of lines.entries()) {
65
+ for (let line_index = 0; line_index < lines.length; line_index += 1) {
74
66
  if (line_index < front_matter_result.body_start) {
75
67
  continue;
76
68
  }
77
69
 
70
+ const line = lines[line_index];
78
71
  const line_number = line_index + 1;
79
72
 
80
73
  if (open_fence) {
@@ -92,7 +85,19 @@ export function parseMarkdownClaims(parse_input, parse_options) {
92
85
  }
93
86
 
94
87
  collectMarkdownLinkClaims(parse_input.path, line, line_number, claims);
95
- collectVisibleDirectiveClaims(parse_input.path, line, line_number, claims);
88
+
89
+ const next_line_index = collectVisibleDirectiveClaims(
90
+ parse_input.path,
91
+ lines,
92
+ line_index,
93
+ claims,
94
+ );
95
+
96
+ if (next_line_index !== null) {
97
+ line_index = next_line_index - 1;
98
+ continue;
99
+ }
100
+
96
101
  collectHiddenDirectiveClaims(parse_input.path, line, line_number, claims);
97
102
  }
98
103
 
@@ -102,6 +107,34 @@ export function parseMarkdownClaims(parse_input, parse_options) {
102
107
  };
103
108
  }
104
109
 
110
+ /**
111
+ * @param {string} file_path
112
+ * @param {string[]} lines
113
+ * @param {number} start_line_index
114
+ * @returns {PatramClaim[]}
115
+ */
116
+ function collectMarkdownMetadataClaims(file_path, lines, start_line_index) {
117
+ /** @type {PatramClaim[]} */
118
+ const claims = [];
119
+ const title_result = getMarkdownTitle(lines, start_line_index);
120
+
121
+ if (title_result) {
122
+ claims.push(createDocumentTitleClaim(file_path, title_result));
123
+ }
124
+
125
+ const description_result = getMarkdownDescription(
126
+ file_path,
127
+ lines,
128
+ title_result,
129
+ );
130
+
131
+ if (description_result) {
132
+ claims.push(createDocumentDescriptionClaim(file_path, description_result));
133
+ }
134
+
135
+ return claims;
136
+ }
137
+
105
138
  /**
106
139
  * @param {string} file_path
107
140
  * @param {string} line
@@ -138,20 +171,25 @@ function collectMarkdownLinkClaims(file_path, line, line_number, claims) {
138
171
 
139
172
  /**
140
173
  * @param {string} file_path
141
- * @param {string} line
142
- * @param {number} line_number
174
+ * @param {string[]} lines
175
+ * @param {number} line_index
143
176
  * @param {PatramClaim[]} claims
177
+ * @returns {number | null}
144
178
  */
145
- function collectVisibleDirectiveClaims(file_path, line, line_number, claims) {
146
- const directive_fields = matchVisibleDirectiveFields(
179
+ function collectVisibleDirectiveClaims(file_path, lines, line_index, claims) {
180
+ const directive_result = collectVisibleDirectiveFields(
147
181
  file_path,
148
- line,
149
- line_number,
182
+ lines,
183
+ line_index,
150
184
  );
151
185
 
152
- if (directive_fields) {
153
- pushDirectiveClaim(file_path, claims, directive_fields);
186
+ if (!directive_result) {
187
+ return null;
154
188
  }
189
+
190
+ pushDirectiveClaim(file_path, claims, directive_result.directive_fields);
191
+
192
+ return directive_result.next_line_index;
155
193
  }
156
194
 
157
195
  /**
@@ -173,36 +211,35 @@ function collectHiddenDirectiveClaims(file_path, line, line_number, claims) {
173
211
  }
174
212
 
175
213
  /**
176
- * @param {string[]} lines
177
- * @param {number} start_line_index
178
- * @returns {{ line: number, value: string } | null}
214
+ * @param {string} file_path
215
+ * @param {{ line: number, value: string }} title_result
216
+ * @returns {PatramClaim}
179
217
  */
180
- function getMarkdownTitle(lines, start_line_index) {
181
- const first_line = lines[start_line_index];
182
-
183
- if (first_line === undefined) {
184
- return null;
185
- }
186
-
187
- const trimmed_line = first_line.trim();
188
-
189
- if (trimmed_line.length === 0) {
190
- return null;
191
- }
192
-
193
- const heading_match = trimmed_line.match(HEADING_PATTERN);
194
-
195
- if (heading_match) {
196
- return {
197
- line: start_line_index + 1,
198
- value: heading_match[1].trim(),
199
- };
200
- }
218
+ function createDocumentTitleClaim(file_path, title_result) {
219
+ return createClaim(file_path, 1, 'document.title', {
220
+ origin: {
221
+ column: 1,
222
+ line: title_result.line,
223
+ path: file_path,
224
+ },
225
+ value: title_result.value,
226
+ });
227
+ }
201
228
 
202
- return {
203
- line: start_line_index + 1,
204
- value: trimmed_line,
205
- };
229
+ /**
230
+ * @param {string} file_path
231
+ * @param {{ column: number, line: number, value: string }} description_result
232
+ * @returns {PatramClaim}
233
+ */
234
+ function createDocumentDescriptionClaim(file_path, description_result) {
235
+ return createClaim(file_path, 2, 'document.description', {
236
+ origin: {
237
+ column: description_result.column,
238
+ line: description_result.line,
239
+ path: file_path,
240
+ },
241
+ value: description_result.value,
242
+ });
206
243
  }
207
244
 
208
245
  /**
@@ -20,15 +20,19 @@ export function parseFrontMatterDirectiveFields(file_path: string, lines: string
20
20
  export function matchVisibleDirectiveFields(file_path: string, line: string, line_number: number): PatramClaimFields | null;
21
21
  /**
22
22
  * @param {string} file_path
23
- * @param {string} line
24
- * @param {number} line_number
25
- * @returns {PatramClaimFields | null}
23
+ * @param {string[]} lines
24
+ * @param {number} line_index
25
+ * @returns {{ directive_fields: PatramClaimFields, next_line_index: number } | null}
26
26
  */
27
- export function matchHiddenDirectiveFields(file_path: string, line: string, line_number: number): PatramClaimFields | null;
27
+ export function collectVisibleDirectiveFields(file_path: string, lines: string[], line_index: number): {
28
+ directive_fields: PatramClaimFields;
29
+ next_line_index: number;
30
+ } | null;
28
31
  /**
29
32
  * @param {string} directive_label
30
- * @returns {string}
33
+ * @returns {string | null}
31
34
  */
32
- export function normalizeDirectiveName(directive_label: string): string;
35
+ export function normalizeDirectiveName(directive_label: string): string | null;
36
+ export function matchHiddenDirectiveFields(...args: unknown[]): PatramClaimFields | null;
33
37
  import type { PatramDiagnostic } from '../../config/load-patram-config.types.d.ts';
34
38
  import type { PatramClaimFields } from '../parse-claims.types.d.ts';
@@ -6,10 +6,16 @@
6
6
  import { parseYamlDirectiveFields } from '../yaml/parse-yaml-claims.js';
7
7
 
8
8
  const FRONT_MATTER_BOUNDARY_PATTERN = /^---$/du;
9
- const MARKDOWN_HIDDEN_DIRECTIVE_PATTERN =
10
- /^\[patram\s+([A-Za-z][A-Za-z0-9 _-]*)=(.+)\]:\s*#\s*$/du;
11
- const LIST_ITEM_DIRECTIVE_PATTERN = /^-\s+([A-Z][A-Za-z _-]*):\s+(.+)$/du;
12
- const VISIBLE_DIRECTIVE_PATTERN = /^([A-Z][A-Za-z _-]*):\s+(.+)$/du;
9
+ const FIELD_NAME_PATTERN = '[a-z][a-z0-9_]*';
10
+ const LIST_ITEM_DIRECTIVE_PATTERN = new RegExp(
11
+ `^-\\s+(${FIELD_NAME_PATTERN}):\\s+(.+)$`,
12
+ 'du',
13
+ );
14
+ const LIST_ITEM_DIRECTIVE_CONTINUATION_PATTERN = /^ {2,3}\S/u;
15
+ const VISIBLE_DIRECTIVE_PATTERN = new RegExp(
16
+ `^(${FIELD_NAME_PATTERN}):\\s+(.+)$`,
17
+ 'du',
18
+ );
13
19
 
14
20
  /**
15
21
  * @param {string} file_path
@@ -85,20 +91,68 @@ export function matchVisibleDirectiveFields(file_path, line, line_number) {
85
91
 
86
92
  /**
87
93
  * @param {string} file_path
88
- * @param {string} line
89
- * @param {number} line_number
90
- * @returns {PatramClaimFields | null}
94
+ * @param {string[]} lines
95
+ * @param {number} line_index
96
+ * @returns {{ directive_fields: PatramClaimFields, next_line_index: number } | null}
91
97
  */
92
- export function matchHiddenDirectiveFields(file_path, line, line_number) {
93
- return matchDirectiveFields(
94
- MARKDOWN_HIDDEN_DIRECTIVE_PATTERN,
95
- 'hidden_tag',
98
+ export function collectVisibleDirectiveFields(file_path, lines, line_index) {
99
+ const line = lines[line_index];
100
+
101
+ if (line === undefined) {
102
+ return null;
103
+ }
104
+
105
+ const directive_fields = matchVisibleDirectiveFields(
96
106
  file_path,
97
107
  line,
98
- line_number,
108
+ line_index + 1,
99
109
  );
110
+
111
+ if (!directive_fields) {
112
+ return null;
113
+ }
114
+
115
+ if (directive_fields.markdown_style !== 'list_item') {
116
+ return {
117
+ directive_fields,
118
+ next_line_index: line_index + 1,
119
+ };
120
+ }
121
+
122
+ const continuation_result = collectListItemDirectiveContinuation(
123
+ lines,
124
+ line_index + 1,
125
+ );
126
+ const directive_value = directive_fields.value;
127
+
128
+ if (
129
+ continuation_result.value.length === 0 ||
130
+ typeof directive_value !== 'string'
131
+ ) {
132
+ return {
133
+ directive_fields,
134
+ next_line_index: continuation_result.next_line_index,
135
+ };
136
+ }
137
+
138
+ return {
139
+ directive_fields: {
140
+ ...directive_fields,
141
+ value: `${directive_value} ${continuation_result.value}`,
142
+ },
143
+ next_line_index: continuation_result.next_line_index,
144
+ };
100
145
  }
101
146
 
147
+ /**
148
+ * @param {...unknown} args
149
+ * @returns {PatramClaimFields | null}
150
+ */
151
+ export const matchHiddenDirectiveFields = (...args) => {
152
+ void args;
153
+ return null;
154
+ };
155
+
102
156
  /**
103
157
  * @param {string[]} lines
104
158
  * @returns {number}
@@ -134,9 +188,15 @@ function matchDirectiveFields(
134
188
  return null;
135
189
  }
136
190
 
191
+ const directive_name = normalizeDirectiveName(directive_match[1]);
192
+
193
+ if (!directive_name) {
194
+ return null;
195
+ }
196
+
137
197
  return {
138
198
  markdown_style,
139
- name: normalizeDirectiveName(directive_match[1]),
199
+ name: directive_name,
140
200
  origin: {
141
201
  column: 1,
142
202
  line: line_number,
@@ -147,13 +207,39 @@ function matchDirectiveFields(
147
207
  };
148
208
  }
149
209
 
210
+ /**
211
+ * @param {string[]} lines
212
+ * @param {number} line_index
213
+ * @returns {{ next_line_index: number, value: string }}
214
+ */
215
+ function collectListItemDirectiveContinuation(lines, line_index) {
216
+ /** @type {string[]} */
217
+ const continuation_lines = [];
218
+ let next_line_index = line_index;
219
+
220
+ while (next_line_index < lines.length) {
221
+ const line = lines[next_line_index];
222
+
223
+ if (!LIST_ITEM_DIRECTIVE_CONTINUATION_PATTERN.test(line)) {
224
+ break;
225
+ }
226
+
227
+ continuation_lines.push(line.trim());
228
+ next_line_index += 1;
229
+ }
230
+
231
+ return {
232
+ next_line_index,
233
+ value: continuation_lines.join(' '),
234
+ };
235
+ }
236
+
150
237
  /**
151
238
  * @param {string} directive_label
152
- * @returns {string}
239
+ * @returns {string | null}
153
240
  */
154
241
  export function normalizeDirectiveName(directive_label) {
155
- return directive_label
156
- .trim()
157
- .toLowerCase()
158
- .replaceAll(/[\s-]+/dgu, '_');
242
+ const normalized_name = directive_label.trim();
243
+
244
+ return /^[a-z][a-z0-9_]*$/du.test(normalized_name) ? normalized_name : null;
159
245
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @param {string[]} lines
3
+ * @param {number} start_line_index
4
+ * @returns {{ is_heading: boolean, line: number, line_index: number, value: string } | null}
5
+ */
6
+ export function getMarkdownTitle(lines: string[], start_line_index: number): {
7
+ is_heading: boolean;
8
+ line: number;
9
+ line_index: number;
10
+ value: string;
11
+ } | null;
12
+ /**
13
+ * @param {string} file_path
14
+ * @param {string[]} lines
15
+ * @param {{ is_heading: boolean, line: number, line_index: number, value: string } | null} title_result
16
+ * @returns {{ column: number, line: number, value: string } | null}
17
+ */
18
+ export function getMarkdownDescription(file_path: string, lines: string[], title_result: {
19
+ is_heading: boolean;
20
+ line: number;
21
+ line_index: number;
22
+ value: string;
23
+ } | null): {
24
+ column: number;
25
+ line: number;
26
+ value: string;
27
+ } | null;
@@ -0,0 +1,243 @@
1
+ import {
2
+ collectVisibleDirectiveFields,
3
+ matchHiddenDirectiveFields,
4
+ } from './parse-markdown-directives.js';
5
+
6
+ const HEADING_PATTERN = /^#{1,6}\s+(.+)$/du;
7
+ const BLOCKQUOTE_PATTERN = /^\s*>/u;
8
+ const INDENTED_CODE_PATTERN = /^(?: {4}|\t)\S/u;
9
+ const MARKDOWN_DESCRIPTION_SENTENCE_PATTERN = /^(.+?[.!?])(?:\s+|$)[\s\S]*$/du;
10
+ const MARKDOWN_DESCRIPTION_LENGTH_LIMIT = 120;
11
+ const MARKDOWN_FENCE_PATTERN = /^([`~]{3,})/du;
12
+ const MARKDOWN_ORDERED_LIST_PATTERN = /^\s*\d+[.)]\s+/u;
13
+ const MARKDOWN_TABLE_PATTERN = /^\s*\|/u;
14
+ const MARKDOWN_UNORDERED_LIST_PATTERN = /^\s*[-+*]\s+/u;
15
+
16
+ /**
17
+ * @param {string[]} lines
18
+ * @param {number} start_line_index
19
+ * @returns {{ is_heading: boolean, line: number, line_index: number, value: string } | null}
20
+ */
21
+ export function getMarkdownTitle(lines, start_line_index) {
22
+ const first_line_index = skipBlankLines(lines, start_line_index);
23
+ const first_line = lines[first_line_index];
24
+
25
+ if (first_line === undefined) {
26
+ return null;
27
+ }
28
+
29
+ const trimmed_line = first_line.trim();
30
+ const heading_match = trimmed_line.match(HEADING_PATTERN);
31
+
32
+ if (heading_match) {
33
+ return {
34
+ is_heading: true,
35
+ line: first_line_index + 1,
36
+ line_index: first_line_index,
37
+ value: heading_match[1].trim(),
38
+ };
39
+ }
40
+
41
+ return {
42
+ is_heading: false,
43
+ line: first_line_index + 1,
44
+ line_index: first_line_index,
45
+ value: trimmed_line,
46
+ };
47
+ }
48
+
49
+ /**
50
+ * @param {string} file_path
51
+ * @param {string[]} lines
52
+ * @param {{ is_heading: boolean, line: number, line_index: number, value: string } | null} title_result
53
+ * @returns {{ column: number, line: number, value: string } | null}
54
+ */
55
+ export function getMarkdownDescription(file_path, lines, title_result) {
56
+ if (!title_result) {
57
+ return null;
58
+ }
59
+
60
+ let line_index = title_result.is_heading
61
+ ? title_result.line_index + 1
62
+ : title_result.line_index;
63
+
64
+ line_index = skipBlankLines(lines, line_index);
65
+
66
+ if (line_index >= lines.length) {
67
+ return null;
68
+ }
69
+
70
+ if (
71
+ title_result.is_heading &&
72
+ isMarkdownDirectiveLine(file_path, lines, line_index)
73
+ ) {
74
+ line_index = skipMarkdownDirectiveBlock(file_path, lines, line_index);
75
+ line_index = skipBlankLines(lines, line_index);
76
+ }
77
+
78
+ const paragraph_result = collectMarkdownParagraph(lines, line_index);
79
+
80
+ if (!paragraph_result) {
81
+ return null;
82
+ }
83
+
84
+ return {
85
+ column: paragraph_result.column,
86
+ line: paragraph_result.line,
87
+ value: summarizeMarkdownParagraph(paragraph_result.value),
88
+ };
89
+ }
90
+
91
+ /**
92
+ * @param {string[]} lines
93
+ * @param {number} line_index
94
+ * @returns {{ column: number, line: number, value: string } | null}
95
+ */
96
+ function collectMarkdownParagraph(lines, line_index) {
97
+ const first_line = lines[line_index];
98
+
99
+ if (first_line === undefined || !isMarkdownParagraphLine(first_line)) {
100
+ return null;
101
+ }
102
+
103
+ const paragraph_lines = [first_line.trim()];
104
+ let next_line_index = line_index + 1;
105
+
106
+ while (next_line_index < lines.length) {
107
+ const next_line = lines[next_line_index];
108
+
109
+ if (next_line.trim().length === 0 || !isMarkdownParagraphLine(next_line)) {
110
+ break;
111
+ }
112
+
113
+ paragraph_lines.push(next_line.trim());
114
+ next_line_index += 1;
115
+ }
116
+
117
+ return {
118
+ column: getLineColumn(first_line),
119
+ line: line_index + 1,
120
+ value: paragraph_lines.join(' '),
121
+ };
122
+ }
123
+
124
+ /**
125
+ * @param {string} file_path
126
+ * @param {string[]} lines
127
+ * @param {number} line_index
128
+ * @returns {boolean}
129
+ */
130
+ function isMarkdownDirectiveLine(file_path, lines, line_index) {
131
+ return (
132
+ collectVisibleDirectiveFields(file_path, lines, line_index) !== null ||
133
+ matchHiddenDirectiveFields(file_path, lines[line_index], line_index + 1) !==
134
+ null
135
+ );
136
+ }
137
+
138
+ /**
139
+ * @param {string} file_path
140
+ * @param {string[]} lines
141
+ * @param {number} line_index
142
+ * @returns {number}
143
+ */
144
+ function skipMarkdownDirectiveBlock(file_path, lines, line_index) {
145
+ let next_line_index = line_index;
146
+
147
+ while (next_line_index < lines.length) {
148
+ const line = lines[next_line_index];
149
+
150
+ if (line.trim().length === 0) {
151
+ next_line_index += 1;
152
+ continue;
153
+ }
154
+
155
+ const visible_directive_result = collectVisibleDirectiveFields(
156
+ file_path,
157
+ lines,
158
+ next_line_index,
159
+ );
160
+
161
+ if (visible_directive_result) {
162
+ next_line_index = visible_directive_result.next_line_index;
163
+ continue;
164
+ }
165
+
166
+ if (
167
+ matchHiddenDirectiveFields(file_path, line, next_line_index + 1) === null
168
+ ) {
169
+ break;
170
+ }
171
+
172
+ next_line_index += 1;
173
+ }
174
+
175
+ return next_line_index;
176
+ }
177
+
178
+ /**
179
+ * @param {string} line
180
+ * @returns {number}
181
+ */
182
+ function getLineColumn(line) {
183
+ const first_character_index = line.search(/\S/u);
184
+
185
+ return first_character_index < 0 ? 1 : first_character_index + 1;
186
+ }
187
+
188
+ /**
189
+ * @param {string} paragraph_text
190
+ * @returns {string}
191
+ */
192
+ function summarizeMarkdownParagraph(paragraph_text) {
193
+ if (paragraph_text.length <= MARKDOWN_DESCRIPTION_LENGTH_LIMIT) {
194
+ return paragraph_text;
195
+ }
196
+
197
+ const sentence_match = paragraph_text.match(
198
+ MARKDOWN_DESCRIPTION_SENTENCE_PATTERN,
199
+ );
200
+
201
+ if (!sentence_match) {
202
+ return paragraph_text;
203
+ }
204
+
205
+ return sentence_match[1].trim();
206
+ }
207
+
208
+ /**
209
+ * @param {string} line
210
+ * @returns {boolean}
211
+ */
212
+ function isMarkdownParagraphLine(line) {
213
+ const trimmed_line = line.trim();
214
+
215
+ if (trimmed_line.length === 0) {
216
+ return false;
217
+ }
218
+
219
+ return !(
220
+ HEADING_PATTERN.test(trimmed_line) ||
221
+ MARKDOWN_FENCE_PATTERN.test(trimmed_line) ||
222
+ MARKDOWN_ORDERED_LIST_PATTERN.test(line) ||
223
+ MARKDOWN_TABLE_PATTERN.test(line) ||
224
+ MARKDOWN_UNORDERED_LIST_PATTERN.test(line) ||
225
+ BLOCKQUOTE_PATTERN.test(line) ||
226
+ INDENTED_CODE_PATTERN.test(line)
227
+ );
228
+ }
229
+
230
+ /**
231
+ * @param {string[]} lines
232
+ * @param {number} start_line_index
233
+ * @returns {number}
234
+ */
235
+ function skipBlankLines(lines, start_line_index) {
236
+ let line_index = start_line_index;
237
+
238
+ while (line_index < lines.length && lines[line_index].trim().length === 0) {
239
+ line_index += 1;
240
+ }
241
+
242
+ return line_index;
243
+ }