patram 0.2.0 → 0.4.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 (38) hide show
  1. package/lib/build-graph-identity.js +86 -99
  2. package/lib/build-graph.js +536 -31
  3. package/lib/build-graph.types.ts +6 -2
  4. package/lib/check-directive-metadata.js +534 -0
  5. package/lib/check-directive-value.js +291 -0
  6. package/lib/check-graph.js +23 -5
  7. package/lib/cli-help-metadata.js +56 -16
  8. package/lib/command-output.js +16 -1
  9. package/lib/derived-summary.js +10 -8
  10. package/lib/directive-diagnostics.js +38 -0
  11. package/lib/directive-type-rules.js +133 -0
  12. package/lib/discover-fields.js +435 -0
  13. package/lib/discover-fields.types.ts +52 -0
  14. package/lib/document-node-identity.js +317 -0
  15. package/lib/format-node-header.js +9 -7
  16. package/lib/format-output-metadata.js +15 -23
  17. package/lib/layout-stored-queries.js +124 -85
  18. package/lib/load-patram-config.js +433 -96
  19. package/lib/load-patram-config.types.ts +98 -3
  20. package/lib/load-project-graph.js +4 -1
  21. package/lib/output-view.types.ts +14 -6
  22. package/lib/parse-cli-arguments.types.ts +1 -1
  23. package/lib/parse-where-clause.js +344 -107
  24. package/lib/parse-where-clause.types.ts +25 -8
  25. package/lib/patram-cli.js +68 -4
  26. package/lib/patram-config.js +31 -31
  27. package/lib/patram-config.types.ts +10 -4
  28. package/lib/query-graph.js +269 -40
  29. package/lib/query-inspection.js +440 -60
  30. package/lib/render-field-discovery.js +184 -0
  31. package/lib/render-json-output.js +21 -22
  32. package/lib/render-output-view.js +301 -34
  33. package/lib/render-plain-output.js +1 -1
  34. package/lib/render-rich-output.js +1 -1
  35. package/lib/render-rich-source.js +245 -14
  36. package/lib/resolve-patram-graph-config.js +15 -9
  37. package/lib/show-document.js +66 -9
  38. package/package.json +5 -5
@@ -128,7 +128,7 @@ function formatPlainStoredQueryLine(line_segments) {
128
128
  */
129
129
  function formatPlainResolvedLinkItem(output_item) {
130
130
  return formatOutputItemBlock({
131
- header: `[${output_item.reference}] document ${output_item.target.path}`,
131
+ header: `[${output_item.reference}] ${output_item.target.kind} ${output_item.target.path ?? output_item.target.id}`,
132
132
  metadata_rows: formatResolvedLinkMetadataRows(output_item.target),
133
133
  metadata_indent: ' ',
134
134
  title: output_item.target.title,
@@ -155,7 +155,7 @@ function formatRichStoredQueryLine(line_segments, ansi) {
155
155
  */
156
156
  function formatRichResolvedLinkItem(output_item, ansi) {
157
157
  return formatOutputItemBlock({
158
- header: `${ansi.gray(`[${output_item.reference}]`)} ${ansi.green(`document ${output_item.target.path}`)}`,
158
+ header: `${ansi.gray(`[${output_item.reference}]`)} ${ansi.green(`${output_item.target.kind} ${output_item.target.path ?? output_item.target.id}`)}`,
159
159
  metadata_rows: formatResolvedLinkMetadataRows(output_item.target),
160
160
  metadata_indent: ' ',
161
161
  title: output_item.target.title,
@@ -46,6 +46,16 @@ const SHIKI_LANGUAGE_NAMES = new Set([
46
46
  ...Object.keys(bundledLanguagesAlias),
47
47
  ]);
48
48
 
49
+ /**
50
+ * @typedef {{
51
+ * ansi: Ansis,
52
+ * next_reference: number,
53
+ * next_top_level_list: number,
54
+ * resolved_links: OutputResolvedLinkItem[],
55
+ * top_level_list_item_gaps: boolean[][]
56
+ * }} RichSourceRenderState
57
+ */
58
+
49
59
  /**
50
60
  * @param {ShowOutputView} output_view
51
61
  * @param {{ color_mode: CliColorMode, color_enabled: boolean }} render_options
@@ -70,11 +80,13 @@ async function renderRichMarkdownSource(output_view, ansi) {
70
80
  const markdown_tree = parseAST(output_view.source);
71
81
  /** @type {string[]} */
72
82
  const rendered_blocks = [];
73
- /** @type {{ ansi: Ansis, next_reference: number, resolved_links: OutputResolvedLinkItem[] }} */
83
+ /** @type {RichSourceRenderState} */
74
84
  const render_state = {
75
85
  ansi,
76
86
  next_reference: 1,
87
+ next_top_level_list: 0,
77
88
  resolved_links: output_view.items,
89
+ top_level_list_item_gaps: collectTopLevelListItemGaps(output_view.source),
78
90
  };
79
91
 
80
92
  for (const node of markdown_tree.nodes) {
@@ -107,7 +119,7 @@ async function renderRichSourceFile(source_path, source_text, ansi) {
107
119
 
108
120
  /**
109
121
  * @param {ComarkNode} node
110
- * @param {{ ansi: Ansis, next_reference: number, resolved_links: OutputResolvedLinkItem[] }} render_state
122
+ * @param {RichSourceRenderState} render_state
111
123
  * @param {number} indent_level
112
124
  * @returns {Promise<string>}
113
125
  */
@@ -131,8 +143,14 @@ async function renderBlockNode(node, render_state, indent_level) {
131
143
  return renderFencedCodeBlock(node, indent_level, render_state.ansi);
132
144
  }
133
145
 
134
- if (node_tag === 'ul' || node_tag === 'ol') {
135
- return renderListBlock(node_tag, node_children, render_state, indent_level);
146
+ if (isListTag(node_tag)) {
147
+ return renderListBlock(
148
+ /** @type {'ol' | 'ul'} */ (node_tag),
149
+ node_children,
150
+ render_state,
151
+ indent_level,
152
+ indent_level === 0 ? getNextTopLevelListItemGaps(render_state) : [],
153
+ );
136
154
  }
137
155
 
138
156
  if (node_tag === 'blockquote') {
@@ -152,7 +170,7 @@ async function renderBlockNode(node, render_state, indent_level) {
152
170
 
153
171
  /**
154
172
  * @param {ComarkNode[]} nodes
155
- * @param {{ ansi: Ansis, next_reference: number, resolved_links: OutputResolvedLinkItem[] }} render_state
173
+ * @param {RichSourceRenderState} render_state
156
174
  * @returns {string}
157
175
  */
158
176
  function renderInlineNodes(nodes, render_state) {
@@ -217,13 +235,23 @@ function renderInlineNodes(nodes, render_state) {
217
235
  /**
218
236
  * @param {'ol' | 'ul'} nodes_type
219
237
  * @param {ComarkNode[]} nodes
220
- * @param {{ ansi: Ansis, next_reference: number, resolved_links: OutputResolvedLinkItem[] }} render_state
238
+ * @param {RichSourceRenderState} render_state
221
239
  * @param {number} indent_level
240
+ * @param {boolean[]} list_item_gaps
222
241
  * @returns {Promise<string>}
223
242
  */
224
- async function renderListBlock(nodes_type, nodes, render_state, indent_level) {
243
+ async function renderListBlock(
244
+ nodes_type,
245
+ nodes,
246
+ render_state,
247
+ indent_level,
248
+ list_item_gaps,
249
+ ) {
225
250
  /** @type {string[]} */
226
251
  const rendered_items = [];
252
+ /** @type {ComarkElement | null} */
253
+ let previous_item = null;
254
+ let rendered_item_count = 0;
227
255
 
228
256
  for (let item_index = 0; item_index < nodes.length; item_index += 1) {
229
257
  const node = nodes[item_index];
@@ -232,6 +260,17 @@ async function renderListBlock(nodes_type, nodes, render_state, indent_level) {
232
260
  continue;
233
261
  }
234
262
 
263
+ if (
264
+ previous_item &&
265
+ shouldRenderListItemGap(
266
+ previous_item,
267
+ node,
268
+ list_item_gaps[rendered_item_count - 1] ?? false,
269
+ )
270
+ ) {
271
+ rendered_items.push('');
272
+ }
273
+
235
274
  rendered_items.push(
236
275
  await renderListItem(
237
276
  node,
@@ -241,6 +280,8 @@ async function renderListBlock(nodes_type, nodes, render_state, indent_level) {
241
280
  indent_level,
242
281
  ),
243
282
  );
283
+ previous_item = node;
284
+ rendered_item_count += 1;
244
285
  }
245
286
 
246
287
  return rendered_items.join('\n');
@@ -250,7 +291,7 @@ async function renderListBlock(nodes_type, nodes, render_state, indent_level) {
250
291
  * @param {ComarkElement} node
251
292
  * @param {number} item_number
252
293
  * @param {boolean} is_ordered
253
- * @param {{ ansi: Ansis, next_reference: number, resolved_links: OutputResolvedLinkItem[] }} render_state
294
+ * @param {RichSourceRenderState} render_state
254
295
  * @param {number} indent_level
255
296
  * @returns {Promise<string>}
256
297
  */
@@ -298,7 +339,7 @@ async function renderListItem(
298
339
 
299
340
  /**
300
341
  * @param {ComarkNode[]} nodes
301
- * @param {{ ansi: Ansis, next_reference: number, resolved_links: OutputResolvedLinkItem[] }} render_state
342
+ * @param {RichSourceRenderState} render_state
302
343
  * @param {number} indent_level
303
344
  * @returns {Promise<string>}
304
345
  */
@@ -461,7 +502,7 @@ function renderHeading(tag_name, nodes, ansi) {
461
502
 
462
503
  /**
463
504
  * @param {ComarkElement} node
464
- * @param {{ ansi: Ansis, next_reference: number, resolved_links: OutputResolvedLinkItem[] }} render_state
505
+ * @param {RichSourceRenderState} render_state
465
506
  * @returns {string}
466
507
  */
467
508
  function renderLinkNode(node, render_state) {
@@ -618,7 +659,11 @@ function renderCodeBlock(
618
659
  content_indent = 0,
619
660
  ) {
620
661
  const label = formatCodeBlockLabel(language_label, file_name);
621
- const content_width = measureCodeBlockWidth(label, source_lines);
662
+ const content_width = measureCodeBlockWidth(
663
+ label,
664
+ source_lines,
665
+ content_indent,
666
+ );
622
667
  /** @type {string[]} */
623
668
  const rendered_lines = [];
624
669
 
@@ -777,7 +822,7 @@ function extractTableRowCells(row_node) {
777
822
 
778
823
  /**
779
824
  * @param {ComarkElement} node
780
- * @param {{ ansi: Ansis, next_reference: number, resolved_links: OutputResolvedLinkItem[] }} render_state
825
+ * @param {RichSourceRenderState} render_state
781
826
  * @returns {{ block_nodes: ComarkElement[], lead_text: string | null }}
782
827
  */
783
828
  function collectListItemParts(node, render_state) {
@@ -836,6 +881,183 @@ function collectListItemParts(node, render_state) {
836
881
  };
837
882
  }
838
883
 
884
+ /**
885
+ * @param {{ next_top_level_list: number, top_level_list_item_gaps: boolean[][] }} render_state
886
+ * @returns {boolean[]}
887
+ */
888
+ function getNextTopLevelListItemGaps(render_state) {
889
+ const list_item_gaps =
890
+ render_state.top_level_list_item_gaps[render_state.next_top_level_list];
891
+
892
+ render_state.next_top_level_list += 1;
893
+
894
+ return list_item_gaps ?? [];
895
+ }
896
+
897
+ /**
898
+ * @param {string} source_text
899
+ * @returns {boolean[][]}
900
+ */
901
+ function collectTopLevelListItemGaps(source_text) {
902
+ const source_lines = source_text.split('\n');
903
+ /** @type {boolean[][]} */
904
+ const top_level_list_item_gaps = [];
905
+ /** @type {boolean[] | null} */
906
+ let current_list_item_gaps = null;
907
+ let saw_blank_line = false;
908
+ /** @type {{ character: '`' | '~', length: number } | null} */
909
+ let active_fence = null;
910
+
911
+ for (const source_line of source_lines) {
912
+ if (active_fence) {
913
+ if (isClosingTopLevelFenceLine(source_line, active_fence)) {
914
+ active_fence = null;
915
+ }
916
+
917
+ continue;
918
+ }
919
+
920
+ const top_level_fence = parseTopLevelFence(source_line);
921
+
922
+ if (top_level_fence) {
923
+ active_fence = top_level_fence;
924
+ current_list_item_gaps = null;
925
+ saw_blank_line = false;
926
+ continue;
927
+ }
928
+
929
+ if (source_line.trim().length === 0) {
930
+ saw_blank_line = current_list_item_gaps !== null;
931
+ continue;
932
+ }
933
+
934
+ if (isTopLevelListItemLine(source_line)) {
935
+ current_list_item_gaps = pushTopLevelListItemGap(
936
+ current_list_item_gaps,
937
+ top_level_list_item_gaps,
938
+ saw_blank_line,
939
+ );
940
+ saw_blank_line = false;
941
+ continue;
942
+ }
943
+
944
+ if (current_list_item_gaps === null) {
945
+ continue;
946
+ }
947
+
948
+ if (source_line.startsWith(' ') || source_line.startsWith('\t')) {
949
+ saw_blank_line = false;
950
+ continue;
951
+ }
952
+
953
+ current_list_item_gaps = null;
954
+ saw_blank_line = false;
955
+ }
956
+
957
+ return top_level_list_item_gaps;
958
+ }
959
+
960
+ /**
961
+ * @param {ComarkElement} previous_item
962
+ * @param {ComarkElement} next_item
963
+ * @param {boolean} has_blank_line_gap
964
+ * @returns {boolean}
965
+ */
966
+ function shouldRenderListItemGap(previous_item, next_item, has_blank_line_gap) {
967
+ if (!has_blank_line_gap) {
968
+ return false;
969
+ }
970
+
971
+ return (
972
+ isSimpleTopLevelListItem(previous_item) &&
973
+ isSimpleTopLevelListItem(next_item)
974
+ );
975
+ }
976
+
977
+ /**
978
+ * @param {ComarkElement} item_node
979
+ * @returns {boolean}
980
+ */
981
+ function isSimpleTopLevelListItem(item_node) {
982
+ const item_children = getElementChildren(item_node);
983
+
984
+ if (item_children.length !== 1) {
985
+ return false;
986
+ }
987
+
988
+ const paragraph_node = item_children[0];
989
+
990
+ if (
991
+ typeof paragraph_node === 'string' ||
992
+ getElementTag(paragraph_node) !== 'p'
993
+ ) {
994
+ return false;
995
+ }
996
+
997
+ return !extractInlineText(getElementChildren(paragraph_node)).includes('\n');
998
+ }
999
+
1000
+ /**
1001
+ * @param {string} source_line
1002
+ * @returns {boolean}
1003
+ */
1004
+ function isTopLevelListItemLine(source_line) {
1005
+ return /^([*+-]|\d+[.)])(?:\s+|$)/du.test(source_line);
1006
+ }
1007
+
1008
+ /**
1009
+ * @param {string} source_line
1010
+ * @returns {{ character: '`' | '~', length: number } | null}
1011
+ */
1012
+ function parseTopLevelFence(source_line) {
1013
+ const fence_match = /^(?<character>`|~)\k<character>{2,}/du.exec(source_line);
1014
+
1015
+ if (!fence_match?.groups?.character) {
1016
+ return null;
1017
+ }
1018
+
1019
+ return {
1020
+ character: /** @type {'`' | '~'} */ (fence_match.groups.character),
1021
+ length: fence_match[0].length,
1022
+ };
1023
+ }
1024
+
1025
+ /**
1026
+ * @param {string} source_line
1027
+ * @param {{ character: '`' | '~', length: number }} active_fence
1028
+ * @returns {boolean}
1029
+ */
1030
+ function isClosingTopLevelFenceLine(source_line, active_fence) {
1031
+ const closing_pattern = new RegExp(
1032
+ `^${active_fence.character}{${active_fence.length},}\\s*$`,
1033
+ 'u',
1034
+ );
1035
+
1036
+ return closing_pattern.test(source_line);
1037
+ }
1038
+
1039
+ /**
1040
+ * @param {boolean[] | null} current_list_item_gaps
1041
+ * @param {boolean[][]} top_level_list_item_gaps
1042
+ * @param {boolean} saw_blank_line
1043
+ * @returns {boolean[]}
1044
+ */
1045
+ function pushTopLevelListItemGap(
1046
+ current_list_item_gaps,
1047
+ top_level_list_item_gaps,
1048
+ saw_blank_line,
1049
+ ) {
1050
+ if (current_list_item_gaps === null) {
1051
+ current_list_item_gaps = [];
1052
+ top_level_list_item_gaps.push(current_list_item_gaps);
1053
+ return current_list_item_gaps;
1054
+ }
1055
+
1056
+ current_list_item_gaps.push(saw_blank_line);
1057
+
1058
+ return current_list_item_gaps;
1059
+ }
1060
+
839
1061
  /**
840
1062
  * @param {string} text
841
1063
  * @param {string} item_prefix
@@ -1001,6 +1223,14 @@ function isHeadingTag(tag_name) {
1001
1223
  return /^h[1-6]$/du.test(tag_name);
1002
1224
  }
1003
1225
 
1226
+ /**
1227
+ * @param {string} tag_name
1228
+ * @returns {boolean}
1229
+ */
1230
+ function isListTag(tag_name) {
1231
+ return tag_name === 'ol' || tag_name === 'ul';
1232
+ }
1233
+
1004
1234
  /**
1005
1235
  * @param {string} tag_name
1006
1236
  * @returns {number}
@@ -1136,13 +1366,14 @@ function renderCodeBlockLine(
1136
1366
  /**
1137
1367
  * @param {string} label
1138
1368
  * @param {string[]} source_lines
1369
+ * @param {number} content_indent
1139
1370
  * @returns {number}
1140
1371
  */
1141
- function measureCodeBlockWidth(label, source_lines) {
1372
+ function measureCodeBlockWidth(label, source_lines, content_indent = 0) {
1142
1373
  return Math.max(
1143
1374
  BLOCK_WIDTH - 2,
1144
1375
  stringWidth(label),
1145
- measureMaxLineWidth(source_lines),
1376
+ measureMaxLineWidth(source_lines) + content_indent,
1146
1377
  );
1147
1378
  }
1148
1379
 
@@ -21,7 +21,7 @@ import { parsePatramConfig } from './patram-config.js';
21
21
  */
22
22
 
23
23
  const BUILT_IN_PATRAM_CONFIG = {
24
- kinds: {
24
+ classes: {
25
25
  document: {
26
26
  builtin: true,
27
27
  label: 'Document',
@@ -30,28 +30,28 @@ const BUILT_IN_PATRAM_CONFIG = {
30
30
  mappings: {
31
31
  'document.title': {
32
32
  node: {
33
+ class: 'document',
33
34
  field: 'title',
34
- kind: 'document',
35
35
  },
36
36
  },
37
37
  'document.description': {
38
38
  node: {
39
+ class: 'document',
39
40
  field: 'description',
40
- kind: 'document',
41
41
  },
42
42
  },
43
43
  'jsdoc.link': {
44
44
  emit: {
45
45
  relation: 'links_to',
46
46
  target: 'path',
47
- target_kind: 'document',
47
+ target_class: 'document',
48
48
  },
49
49
  },
50
50
  'markdown.link': {
51
51
  emit: {
52
52
  relation: 'links_to',
53
53
  target: 'path',
54
- target_kind: 'document',
54
+ target_class: 'document',
55
55
  },
56
56
  },
57
57
  },
@@ -71,10 +71,10 @@ const BUILT_IN_PATRAM_CONFIG = {
71
71
  * @returns {PatramConfig}
72
72
  */
73
73
  export function resolvePatramGraphConfig(repo_config) {
74
- return parsePatramConfig({
75
- kinds: {
76
- ...BUILT_IN_PATRAM_CONFIG.kinds,
77
- ...repo_config.kinds,
74
+ const graph_config = parsePatramConfig({
75
+ classes: {
76
+ ...BUILT_IN_PATRAM_CONFIG.classes,
77
+ ...repo_config.classes,
78
78
  },
79
79
  mappings: {
80
80
  ...BUILT_IN_PATRAM_CONFIG.mappings,
@@ -85,4 +85,10 @@ export function resolvePatramGraphConfig(repo_config) {
85
85
  ...repo_config.relations,
86
86
  },
87
87
  });
88
+
89
+ return {
90
+ ...graph_config,
91
+ class_schemas: repo_config.class_schemas,
92
+ fields: repo_config.fields,
93
+ };
88
94
  }
@@ -8,6 +8,7 @@
8
8
  import { readFile } from 'node:fs/promises';
9
9
  import { posix, relative, resolve } from 'node:path';
10
10
 
11
+ import { resolveDocumentNodeId } from './build-graph-identity.js';
11
12
  import { parseSourceFile } from './parse-claims.js';
12
13
 
13
14
  /**
@@ -92,6 +93,7 @@ export async function loadShowOutput(
92
93
  source_file_path,
93
94
  source_text,
94
95
  parse_result.claims,
96
+ graph.document_node_ids,
95
97
  graph.nodes,
96
98
  ),
97
99
  };
@@ -101,10 +103,17 @@ export async function loadShowOutput(
101
103
  * @param {string} source_file_path
102
104
  * @param {string} source_text
103
105
  * @param {PatramClaim[]} claims
106
+ * @param {import('./build-graph.types.ts').BuildGraphResult['document_node_ids']} document_node_ids
104
107
  * @param {Record<string, GraphNode>} graph_nodes
105
108
  * @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
109
  */
107
- function createShowOutput(source_file_path, source_text, claims, graph_nodes) {
110
+ function createShowOutput(
111
+ source_file_path,
112
+ source_text,
113
+ claims,
114
+ document_node_ids,
115
+ graph_nodes,
116
+ ) {
108
117
  const link_claims = claims.filter(isResolvedLinkClaim);
109
118
  const rendered_link_claims = link_claims.filter(isMarkdownLinkClaim);
110
119
  const resolved_links = link_claims.map((claim, claim_index) =>
@@ -112,6 +121,7 @@ function createShowOutput(source_file_path, source_text, claims, graph_nodes) {
112
121
  source_file_path,
113
122
  claim,
114
123
  claim_index + 1,
124
+ document_node_ids,
115
125
  graph_nodes,
116
126
  ),
117
127
  );
@@ -197,6 +207,7 @@ function renderResolvedSourceLine(
197
207
  * @param {string} source_file_path
198
208
  * @param {PatramClaim} claim
199
209
  * @param {number} reference
210
+ * @param {import('./build-graph.types.ts').BuildGraphResult['document_node_ids']} document_node_ids
200
211
  * @param {Record<string, GraphNode>} graph_nodes
201
212
  * @returns {{ label: string, reference: number, target: { kind?: string, path: string, status?: string, title: string } }}
202
213
  */
@@ -204,6 +215,7 @@ function createResolvedLinkSummary(
204
215
  source_file_path,
205
216
  claim,
206
217
  reference,
218
+ document_node_ids,
207
219
  graph_nodes,
208
220
  ) {
209
221
  const claim_value = getLinkClaimValue(claim);
@@ -211,18 +223,17 @@ function createResolvedLinkSummary(
211
223
  source_file_path,
212
224
  claim_value.target,
213
225
  );
214
- const target_node = graph_nodes[`doc:${target_path}`];
215
- const target_title = target_node?.title ?? claim_value.text;
226
+ const target_node =
227
+ graph_nodes[resolveDocumentNodeId(document_node_ids, target_path)];
216
228
 
217
229
  return {
218
230
  label: claim_value.text,
219
231
  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
- },
232
+ target: createResolvedLinkTarget(
233
+ target_node,
234
+ target_path,
235
+ claim_value.text,
236
+ ),
226
237
  };
227
238
  }
228
239
 
@@ -239,6 +250,52 @@ function resolveShowTargetPath(source_file_path, raw_target) {
239
250
  return normalizeRepoRelativePath(posix.join(source_directory, raw_target));
240
251
  }
241
252
 
253
+ /**
254
+ * @param {string | string[] | undefined} field_value
255
+ * @returns {string | undefined}
256
+ */
257
+ function getScalarGraphField(field_value) {
258
+ if (Array.isArray(field_value)) {
259
+ return field_value[0];
260
+ }
261
+
262
+ return field_value;
263
+ }
264
+
265
+ /**
266
+ * @param {GraphNode | undefined} target_node
267
+ * @param {string} target_path
268
+ * @param {string} fallback_title
269
+ * @returns {{ kind?: string, path: string, status?: string, title: string }}
270
+ */
271
+ function createResolvedLinkTarget(target_node, target_path, fallback_title) {
272
+ return {
273
+ kind: getResolvedLinkTargetKind(target_node),
274
+ path: getResolvedLinkTargetPath(target_node, target_path),
275
+ status: getScalarGraphField(target_node?.status),
276
+ title: getScalarGraphField(target_node?.title) ?? fallback_title,
277
+ };
278
+ }
279
+
280
+ /**
281
+ * @param {GraphNode | undefined} target_node
282
+ * @returns {string | undefined}
283
+ */
284
+ function getResolvedLinkTargetKind(target_node) {
285
+ return getScalarGraphField(target_node?.$class ?? target_node?.kind);
286
+ }
287
+
288
+ /**
289
+ * @param {GraphNode | undefined} target_node
290
+ * @param {string} target_path
291
+ * @returns {string}
292
+ */
293
+ function getResolvedLinkTargetPath(target_node, target_path) {
294
+ return (
295
+ getScalarGraphField(target_node?.$path ?? target_node?.path) ?? target_path
296
+ );
297
+ }
298
+
242
299
  /**
243
300
  * @param {PatramClaim} claim
244
301
  * @returns {claim is PatramClaim & { type: 'markdown.link', value: { target: string, text: string } }}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patram",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "main": "./lib/patram.js",
6
6
  "exports": {
@@ -65,8 +65,8 @@
65
65
  "devDependencies": {
66
66
  "@eslint/js": "^10.0.1",
67
67
  "@types/node": "^24.12.0",
68
- "@vitest/coverage-v8": "^4.1.0",
69
- "eslint": "^10.0.3",
68
+ "@vitest/coverage-v8": "^4.1.1",
69
+ "eslint": "^10.1.0",
70
70
  "eslint-plugin-jsdoc": "^62.8.0",
71
71
  "globals": "^17.4.0",
72
72
  "husky": "^9.1.7",
@@ -74,7 +74,7 @@
74
74
  "lint-staged": "^16.2.6",
75
75
  "prettier": "^3.5.3",
76
76
  "slice-ansi": "^8.0.0",
77
- "typescript": "^5.8.2",
78
- "vitest": "^4.1.0"
77
+ "typescript": "^6.0.2",
78
+ "vitest": "^4.1.1"
79
79
  }
80
80
  }