patram 0.3.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.
- package/lib/build-graph-identity.js +70 -84
- package/lib/build-graph.js +170 -18
- package/lib/build-graph.types.ts +1 -0
- package/lib/check-directive-metadata.js +20 -2
- package/lib/check-directive-value.js +9 -0
- package/lib/check-graph.js +1 -2
- package/lib/cli-help-metadata.js +12 -0
- package/lib/command-output.js +16 -1
- package/lib/discover-fields.js +9 -1
- package/lib/document-node-identity.js +317 -0
- package/lib/layout-stored-queries.js +122 -29
- package/lib/load-patram-config.js +1 -1
- package/lib/parse-where-clause.js +237 -66
- package/lib/parse-where-clause.types.ts +21 -6
- package/lib/patram-cli.js +34 -2
- package/lib/query-graph.js +29 -19
- package/lib/query-inspection.js +173 -68
- package/lib/render-field-discovery.js +44 -8
- package/lib/render-output-view.js +65 -19
- package/lib/render-rich-source.js +245 -14
- package/lib/show-document.js +15 -2
- package/package.json +1 -1
|
@@ -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 {
|
|
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 {
|
|
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
|
|
135
|
-
return renderListBlock(
|
|
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 {
|
|
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 {
|
|
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(
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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(
|
|
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 {
|
|
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
|
|
package/lib/show-document.js
CHANGED
|
@@ -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(
|
|
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,7 +223,8 @@ function createResolvedLinkSummary(
|
|
|
211
223
|
source_file_path,
|
|
212
224
|
claim_value.target,
|
|
213
225
|
);
|
|
214
|
-
const target_node =
|
|
226
|
+
const target_node =
|
|
227
|
+
graph_nodes[resolveDocumentNodeId(document_node_ids, target_path)];
|
|
215
228
|
|
|
216
229
|
return {
|
|
217
230
|
label: claim_value.text,
|