@real1ty-obsidian-plugins/utils 2.4.0 → 2.6.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 (66) hide show
  1. package/dist/core/evaluator/base.d.ts +22 -0
  2. package/dist/core/evaluator/base.d.ts.map +1 -0
  3. package/dist/core/evaluator/base.js +52 -0
  4. package/dist/core/evaluator/base.js.map +1 -0
  5. package/dist/core/evaluator/color.d.ts +19 -0
  6. package/dist/core/evaluator/color.d.ts.map +1 -0
  7. package/dist/core/evaluator/color.js +25 -0
  8. package/dist/core/evaluator/color.js.map +1 -0
  9. package/dist/core/evaluator/excluded.d.ts +32 -0
  10. package/dist/core/evaluator/excluded.d.ts.map +1 -0
  11. package/dist/core/evaluator/excluded.js +41 -0
  12. package/dist/core/evaluator/excluded.js.map +1 -0
  13. package/dist/core/evaluator/filter.d.ts +15 -0
  14. package/dist/core/evaluator/filter.d.ts.map +1 -0
  15. package/dist/core/evaluator/filter.js +27 -0
  16. package/dist/core/evaluator/filter.js.map +1 -0
  17. package/dist/core/evaluator/included.d.ts +36 -0
  18. package/dist/core/evaluator/included.d.ts.map +1 -0
  19. package/dist/core/evaluator/included.js +51 -0
  20. package/dist/core/evaluator/included.js.map +1 -0
  21. package/dist/core/evaluator/index.d.ts +6 -0
  22. package/dist/core/evaluator/index.d.ts.map +1 -0
  23. package/dist/core/evaluator/index.js +6 -0
  24. package/dist/core/evaluator/index.js.map +1 -0
  25. package/dist/core/expression-utils.d.ts +17 -0
  26. package/dist/core/expression-utils.d.ts.map +1 -0
  27. package/dist/core/expression-utils.js +40 -0
  28. package/dist/core/expression-utils.js.map +1 -0
  29. package/dist/core/frontmatter-value.d.ts +143 -0
  30. package/dist/core/frontmatter-value.d.ts.map +1 -0
  31. package/dist/core/frontmatter-value.js +408 -0
  32. package/dist/core/frontmatter-value.js.map +1 -0
  33. package/dist/core/index.d.ts +3 -1
  34. package/dist/core/index.d.ts.map +1 -1
  35. package/dist/core/index.js +3 -1
  36. package/dist/core/index.js.map +1 -1
  37. package/dist/file/index.d.ts +1 -0
  38. package/dist/file/index.d.ts.map +1 -1
  39. package/dist/file/index.js +1 -0
  40. package/dist/file/index.js.map +1 -1
  41. package/dist/file/link-parser.d.ts +26 -0
  42. package/dist/file/link-parser.d.ts.map +1 -1
  43. package/dist/file/link-parser.js +59 -0
  44. package/dist/file/link-parser.js.map +1 -1
  45. package/dist/file/property-utils.d.ts +55 -0
  46. package/dist/file/property-utils.d.ts.map +1 -0
  47. package/dist/file/property-utils.js +90 -0
  48. package/dist/file/property-utils.js.map +1 -0
  49. package/package.json +2 -1
  50. package/src/core/evaluator/base.ts +71 -0
  51. package/src/core/evaluator/color.ts +37 -0
  52. package/src/core/evaluator/excluded.ts +63 -0
  53. package/src/core/evaluator/filter.ts +33 -0
  54. package/src/core/evaluator/included.ts +74 -0
  55. package/src/core/evaluator/index.ts +5 -0
  56. package/src/core/expression-utils.ts +53 -0
  57. package/src/core/frontmatter-value.ts +528 -0
  58. package/src/core/index.ts +3 -1
  59. package/src/file/index.ts +1 -0
  60. package/src/file/link-parser.ts +73 -0
  61. package/src/file/property-utils.ts +114 -0
  62. package/dist/core/evaluator-base.d.ts +0 -52
  63. package/dist/core/evaluator-base.d.ts.map +0 -1
  64. package/dist/core/evaluator-base.js +0 -84
  65. package/dist/core/evaluator-base.js.map +0 -1
  66. package/src/core/evaluator-base.ts +0 -118
@@ -0,0 +1,528 @@
1
+ // ============================================================================
2
+ // Value Checking
3
+ // ============================================================================
4
+
5
+ export function isEmptyValue(value: unknown): boolean {
6
+ if (value === null || value === undefined) {
7
+ return true;
8
+ }
9
+
10
+ if (typeof value === "string" && value.trim() === "") {
11
+ return true;
12
+ }
13
+
14
+ if (Array.isArray(value) && value.length === 0) {
15
+ return true;
16
+ }
17
+
18
+ return false;
19
+ }
20
+
21
+ // ============================================================================
22
+ // Value Serialization & Parsing (for editing in input fields)
23
+ // ============================================================================
24
+
25
+ /**
26
+ * Serializes a frontmatter value to a string for editing in input fields.
27
+ * Arrays are joined with ", " for easier editing.
28
+ */
29
+ export function serializeValue(value: unknown): string {
30
+ if (value === null || value === undefined) {
31
+ return "";
32
+ }
33
+
34
+ if (Array.isArray(value)) {
35
+ return value.map((item) => serializeValue(item)).join(", ");
36
+ }
37
+
38
+ if (typeof value === "object") {
39
+ return JSON.stringify(value);
40
+ }
41
+
42
+ return String(value);
43
+ }
44
+
45
+ /**
46
+ * Parses a string value from an input field into the appropriate type.
47
+ * Handles: booleans, numbers, JSON objects/arrays, comma-separated arrays, and strings.
48
+ */
49
+ export function parseValue(rawValue: string): unknown {
50
+ const trimmed = rawValue.trim();
51
+
52
+ if (trimmed === "") {
53
+ return "";
54
+ }
55
+
56
+ // Parse boolean
57
+ if (trimmed.toLowerCase() === "true") {
58
+ return true;
59
+ }
60
+ if (trimmed.toLowerCase() === "false") {
61
+ return false;
62
+ }
63
+
64
+ // Parse number
65
+ if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
66
+ const num = Number(trimmed);
67
+ if (!Number.isNaN(num)) {
68
+ return num;
69
+ }
70
+ }
71
+
72
+ // Parse JSON object or array (check BEFORE comma-separated arrays)
73
+ if (
74
+ (trimmed.startsWith("{") && trimmed.endsWith("}")) ||
75
+ (trimmed.startsWith("[") && trimmed.endsWith("]"))
76
+ ) {
77
+ try {
78
+ return JSON.parse(trimmed);
79
+ } catch {
80
+ // If parsing fails, continue to other checks
81
+ }
82
+ }
83
+
84
+ // Parse comma-separated array
85
+ if (trimmed.includes(",")) {
86
+ const items = trimmed.split(",").map((item) => item.trim());
87
+
88
+ if (items.every((item) => item.length > 0)) {
89
+ return items;
90
+ }
91
+ }
92
+
93
+ // Default: return as string
94
+ return trimmed;
95
+ }
96
+
97
+ // ============================================================================
98
+ // Value Formatting (for display in read-only contexts)
99
+ // ============================================================================
100
+
101
+ /**
102
+ * Formats a frontmatter value for display in read-only contexts.
103
+ * Converts booleans to "Yes"/"No", numbers to strings, and objects to JSON.
104
+ */
105
+ export function formatValue(value: unknown): string {
106
+ if (typeof value === "boolean") {
107
+ return value ? "Yes" : "No";
108
+ }
109
+
110
+ if (typeof value === "number") {
111
+ return value.toString();
112
+ }
113
+
114
+ if (typeof value === "object" && value !== null) {
115
+ return JSON.stringify(value, null, 2);
116
+ }
117
+
118
+ return String(value);
119
+ }
120
+
121
+ // ============================================================================
122
+ // Wiki Link Parsing
123
+ // ============================================================================
124
+
125
+ /**
126
+ * Parses wiki link syntax from a string value.
127
+ * Supports both [[path]] and [[path|alias]] formats.
128
+ * Returns null if the string is not a wiki link.
129
+ */
130
+ export function parseWikiLinkWithDisplay(
131
+ value: string
132
+ ): { linkPath: string; displayText: string } | null {
133
+ const wikiLinkMatch = value.match(/^\[\[([^\]]*)\]\]$/);
134
+ if (!wikiLinkMatch) {
135
+ return null;
136
+ }
137
+
138
+ const innerContent = wikiLinkMatch[1];
139
+ const pipeIndex = innerContent.indexOf("|");
140
+
141
+ const linkPath =
142
+ pipeIndex !== -1 ? innerContent.substring(0, pipeIndex).trim() : innerContent.trim();
143
+
144
+ const displayText = pipeIndex !== -1 ? innerContent.substring(pipeIndex + 1).trim() : linkPath;
145
+
146
+ return { linkPath, displayText };
147
+ }
148
+
149
+ // ============================================================================
150
+ // Property Normalization
151
+ // ============================================================================
152
+
153
+ /**
154
+ * Normalizes frontmatter property values to an array of strings.
155
+ * Handles various YAML formats and ensures consistent output.
156
+ *
157
+ * @param value - The raw frontmatter property value (can be any type)
158
+ * @param propertyName - Optional property name for logging purposes
159
+ * @returns Array of strings, or empty array if value is invalid/unexpected
160
+ *
161
+ * @example
162
+ * // Single string value
163
+ * normalizeProperty("[[link]]") // ["[[link]]"]
164
+ *
165
+ * // Array of strings
166
+ * normalizeProperty(["[[link1]]", "[[link2]]"]) // ["[[link1]]", "[[link2]]"]
167
+ *
168
+ * // Mixed array (filters out non-strings)
169
+ * normalizeProperty(["[[link]]", 42, null]) // ["[[link]]"]
170
+ *
171
+ * // Invalid types
172
+ * normalizeProperty(null) // []
173
+ * normalizeProperty(undefined) // []
174
+ * normalizeProperty(42) // []
175
+ * normalizeProperty({}) // []
176
+ */
177
+ export function normalizeProperty(value: unknown, propertyName?: string): string[] {
178
+ // Handle undefined and null
179
+ if (value === undefined || value === null) {
180
+ return [];
181
+ }
182
+
183
+ // Handle string values - convert to single-item array
184
+ if (typeof value === "string") {
185
+ // Empty strings should return empty array
186
+ if (value.trim() === "") {
187
+ return [];
188
+ }
189
+ return [value];
190
+ }
191
+
192
+ // Handle array values
193
+ if (Array.isArray(value)) {
194
+ // Empty arrays
195
+ if (value.length === 0) {
196
+ return [];
197
+ }
198
+
199
+ // Filter to only string values
200
+ const stringValues = value.filter((item): item is string => {
201
+ if (typeof item === "string") {
202
+ return true;
203
+ }
204
+
205
+ // Log warning for non-string items
206
+ if (propertyName) {
207
+ console.warn(
208
+ `Property "${propertyName}" contains non-string value (${typeof item}), filtering it out:`,
209
+ item
210
+ );
211
+ }
212
+ return false;
213
+ });
214
+
215
+ // Filter out empty strings
216
+ const nonEmptyStrings = stringValues.filter((s) => s.trim() !== "");
217
+
218
+ return nonEmptyStrings;
219
+ }
220
+
221
+ // Handle unexpected types (numbers, booleans, objects, etc.)
222
+ if (propertyName) {
223
+ console.warn(
224
+ `Property "${propertyName}" has unexpected type (${typeof value}), returning empty array. Value:`,
225
+ value
226
+ );
227
+ }
228
+
229
+ return [];
230
+ }
231
+
232
+ /**
233
+ * Batch normalize multiple property values from frontmatter.
234
+ * Useful for processing multiple properties at once.
235
+ *
236
+ * @param frontmatter - The frontmatter object
237
+ * @param propertyNames - Array of property names to normalize
238
+ * @returns Map of property names to normalized string arrays
239
+ *
240
+ * @example
241
+ * const frontmatter = {
242
+ * parent: "[[Parent]]",
243
+ * children: ["[[Child1]]", "[[Child2]]"],
244
+ * related: null
245
+ * };
246
+ *
247
+ * const normalized = normalizeProperties(frontmatter, ["parent", "children", "related"]);
248
+ * // Map {
249
+ * // "parent" => ["[[Parent]]"],
250
+ * // "children" => ["[[Child1]]", "[[Child2]]"],
251
+ * // "related" => []
252
+ * // }
253
+ */
254
+ export function normalizeProperties(
255
+ frontmatter: Record<string, unknown>,
256
+ propertyNames: string[]
257
+ ): Map<string, string[]> {
258
+ const result = new Map<string, string[]>();
259
+
260
+ for (const propName of propertyNames) {
261
+ const value = frontmatter[propName];
262
+ result.set(propName, normalizeProperty(value, propName));
263
+ }
264
+
265
+ return result;
266
+ }
267
+
268
+ // ============================================================================
269
+ // String Utilities
270
+ // ============================================================================
271
+
272
+ /**
273
+ * Truncates a string to a maximum length, adding ellipsis if needed.
274
+ */
275
+ export function truncateString(text: string, maxLength: number): string {
276
+ return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text;
277
+ }
278
+
279
+ /**
280
+ * Removes wiki link syntax from a string for cleaner display.
281
+ * Converts [[Link|Alias]] to just "Link" or [[Link]] to "Link".
282
+ */
283
+ export function removeWikiLinks(text: string): string {
284
+ return text.replace(/\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g, "$1");
285
+ }
286
+
287
+ // ============================================================================
288
+ // Array Formatting Utilities
289
+ // ============================================================================
290
+
291
+ /**
292
+ * Formats an array as a compact comma-separated string with smart truncation.
293
+ * Shows "item1, item2, +3" if the full list would exceed maxLength.
294
+ */
295
+ export function formatArrayCompact(items: string[], maxLength: number): string {
296
+ if (items.length === 0) {
297
+ return "";
298
+ }
299
+
300
+ // Single item - just truncate it
301
+ if (items.length === 1) {
302
+ return truncateString(items[0], maxLength);
303
+ }
304
+
305
+ const joined = items.join(", ");
306
+
307
+ // Fits within limit - return as is
308
+ if (joined.length <= maxLength) {
309
+ return joined;
310
+ }
311
+
312
+ // Too long - show first few items + count
313
+ let result = "";
314
+ let count = 0;
315
+
316
+ for (const item of items) {
317
+ const testResult = result ? `${result}, ${item}` : item;
318
+
319
+ if (testResult.length > maxLength - 5) {
320
+ const remaining = items.length - count;
321
+ return `${result}${remaining > 0 ? `, +${remaining}` : ""}`;
322
+ }
323
+
324
+ result = testResult;
325
+ count++;
326
+ }
327
+
328
+ return result;
329
+ }
330
+
331
+ // ============================================================================
332
+ // Property Filtering (Generic over Settings)
333
+ // ============================================================================
334
+
335
+ export interface DisplaySettings {
336
+ hideUnderscoreProperties?: boolean;
337
+ hideEmptyProperties?: boolean;
338
+ }
339
+
340
+ /**
341
+ * Filters frontmatter properties based on display settings.
342
+ * Returns an array of [key, value] pairs that should be displayed.
343
+ */
344
+ export function filterPropertiesForDisplay<TSettings extends DisplaySettings>(
345
+ frontmatter: Record<string, unknown>,
346
+ settings: TSettings
347
+ ): Array<[string, unknown]> {
348
+ const entries = Object.entries(frontmatter);
349
+
350
+ return entries.filter(([key, value]) => {
351
+ // Hide underscore properties if configured
352
+ if (settings.hideUnderscoreProperties && key.startsWith("_")) {
353
+ return false;
354
+ }
355
+
356
+ // Hide empty properties if configured
357
+ if (settings.hideEmptyProperties && isEmptyValue(value)) {
358
+ return false;
359
+ }
360
+
361
+ return true;
362
+ });
363
+ }
364
+
365
+ /**
366
+ * Filters a specific list of property names from frontmatter.
367
+ * Useful when you want to display only specific properties (like in tooltips).
368
+ */
369
+ export function filterSpecificProperties<TSettings extends DisplaySettings>(
370
+ frontmatter: Record<string, unknown>,
371
+ propertyNames: string[],
372
+ settings: TSettings
373
+ ): Array<{ key: string; value: unknown }> {
374
+ const result: Array<{ key: string; value: unknown }> = [];
375
+
376
+ for (const propName of propertyNames) {
377
+ // Skip if property doesn't exist in frontmatter
378
+ if (!(propName in frontmatter)) {
379
+ continue;
380
+ }
381
+
382
+ const value = frontmatter[propName];
383
+
384
+ // Hide underscore properties if configured
385
+ if (settings.hideUnderscoreProperties && propName.startsWith("_")) {
386
+ continue;
387
+ }
388
+
389
+ // Hide empty properties if configured
390
+ if (settings.hideEmptyProperties && isEmptyValue(value)) {
391
+ continue;
392
+ }
393
+
394
+ result.push({ key: propName, value });
395
+ }
396
+
397
+ return result;
398
+ }
399
+
400
+ // ============================================================================
401
+ // Inline Wiki Link Parsing
402
+ // ============================================================================
403
+
404
+ export interface WikiLinkSegment {
405
+ type: "text" | "link";
406
+ content: string;
407
+ linkPath?: string;
408
+ displayText?: string;
409
+ }
410
+
411
+ /**
412
+ * Parses a string containing inline wiki links into segments.
413
+ * Useful for rendering strings with clickable wiki links mixed with regular text.
414
+ *
415
+ * @example
416
+ * parseInlineWikiLinks("Visit [[Page1]] and [[Page2|Second Page]]")
417
+ * // Returns:
418
+ * // [
419
+ * // { type: "text", content: "Visit " },
420
+ * // { type: "link", content: "[[Page1]]", linkPath: "Page1", displayText: "Page1" },
421
+ * // { type: "text", content: " and " },
422
+ * // { type: "link", content: "[[Page2|Second Page]]", linkPath: "Page2", displayText: "Second Page" }
423
+ * // ]
424
+ */
425
+ export function parseInlineWikiLinks(text: string): WikiLinkSegment[] {
426
+ const segments: WikiLinkSegment[] = [];
427
+ const wikiLinkRegex = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
428
+ let lastIndex = 0;
429
+
430
+ const matches = text.matchAll(wikiLinkRegex);
431
+
432
+ for (const match of matches) {
433
+ // Add text before the link
434
+ if (match.index !== undefined && match.index > lastIndex) {
435
+ segments.push({
436
+ type: "text",
437
+ content: text.substring(lastIndex, match.index),
438
+ });
439
+ }
440
+
441
+ // Add the link segment
442
+ const linkPath = match[1];
443
+ const displayText = match[2] || linkPath;
444
+
445
+ segments.push({
446
+ type: "link",
447
+ content: match[0],
448
+ linkPath,
449
+ displayText,
450
+ });
451
+
452
+ lastIndex = (match.index ?? 0) + match[0].length;
453
+ }
454
+
455
+ // Add remaining text
456
+ if (lastIndex < text.length) {
457
+ segments.push({
458
+ type: "text",
459
+ content: text.substring(lastIndex),
460
+ });
461
+ }
462
+
463
+ // If no links found, return the entire text as a single segment
464
+ if (segments.length === 0) {
465
+ segments.push({
466
+ type: "text",
467
+ content: text,
468
+ });
469
+ }
470
+
471
+ return segments;
472
+ }
473
+
474
+ // ============================================================================
475
+ // Node Display Formatting
476
+ // ============================================================================
477
+
478
+ /**
479
+ * Formats a frontmatter value for compact display inside graph nodes.
480
+ * Truncates long values and handles arrays gracefully.
481
+ *
482
+ * @param value - The frontmatter value to format
483
+ * @param maxLength - Maximum length before truncation (default: 20)
484
+ * @returns Formatted string suitable for node display
485
+ *
486
+ * @example
487
+ * formatValueForNode("completed") // "completed"
488
+ * formatValueForNode("A very long string that exceeds the limit") // "A very long string..."
489
+ * formatValueForNode(["tag1", "tag2", "tag3"]) // "tag1, tag2, tag3"
490
+ * formatValueForNode(["tag1", "tag2", "tag3", "tag4", "tag5"], 15) // "tag1, tag2, +3"
491
+ * formatValueForNode(true) // "Yes"
492
+ * formatValueForNode(42) // "42"
493
+ */
494
+ export function formatValueForNode(value: unknown, maxLength = 20): string {
495
+ if (isEmptyValue(value)) {
496
+ return "";
497
+ }
498
+
499
+ // Booleans: reuse formatValue logic
500
+ if (typeof value === "boolean") {
501
+ return value ? "Yes" : "No";
502
+ }
503
+
504
+ // Numbers: reuse formatValue logic
505
+ if (typeof value === "number") {
506
+ return value.toString();
507
+ }
508
+
509
+ // Arrays: extract strings and format compactly
510
+ if (Array.isArray(value)) {
511
+ const stringValues = value.filter((item): item is string => typeof item === "string");
512
+ return formatArrayCompact(stringValues, maxLength);
513
+ }
514
+
515
+ // Strings: clean wiki links and truncate
516
+ if (typeof value === "string") {
517
+ const cleaned = removeWikiLinks(value);
518
+ return truncateString(cleaned, maxLength);
519
+ }
520
+
521
+ // Objects: stringify and truncate
522
+ if (typeof value === "object" && value !== null) {
523
+ const jsonStr = JSON.stringify(value);
524
+ return truncateString(jsonStr, maxLength);
525
+ }
526
+
527
+ return String(value);
528
+ }
package/src/core/index.ts CHANGED
@@ -1,2 +1,4 @@
1
- export * from "./evaluator-base";
1
+ export * from "./evaluator";
2
+ export * from "./expression-utils";
3
+ export * from "./frontmatter-value";
2
4
  export * from "./generate";
package/src/file/index.ts CHANGED
@@ -3,4 +3,5 @@ export * from "./file";
3
3
  export * from "./file-operations";
4
4
  export * from "./frontmatter";
5
5
  export * from "./link-parser";
6
+ export * from "./property-utils";
6
7
  export * from "./templater";
@@ -16,3 +16,76 @@ export const extractFilePathFromLink = (link: string): string | null => {
16
16
 
17
17
  return filePath.endsWith(".md") ? filePath : `${filePath}.md`;
18
18
  };
19
+
20
+ /**
21
+ * Parses an Obsidian wiki link to extract the file path.
22
+ * Handles formats like:
23
+ * - [[path/to/file]]
24
+ * - [[path/to/file|Display Name]]
25
+ *
26
+ * @param link - The wiki link string
27
+ * @returns The file path without the [[ ]] brackets, or null if invalid
28
+ */
29
+ export function parseWikiLink(link: string): string | null {
30
+ if (!link || typeof link !== "string") {
31
+ return null;
32
+ }
33
+
34
+ const trimmed = link.trim();
35
+
36
+ // Match [[path/to/file]] or [[path/to/file|Display Name]]
37
+ const match = trimmed.match(/^\[\[([^\]|]+)(?:\|[^\]]+)?\]\]$/);
38
+
39
+ if (!match) {
40
+ return null;
41
+ }
42
+
43
+ return match[1].trim();
44
+ }
45
+
46
+ /**
47
+ * Parses a property value that can be a single link or an array of links.
48
+ * Extracts file paths from all valid wiki links.
49
+ *
50
+ * @param value - The property value (string, string[], or undefined)
51
+ * @returns Array of file paths, empty if no valid links found
52
+ */
53
+ export function parsePropertyLinks(value: string | string[] | undefined): string[] {
54
+ if (!value) {
55
+ return [];
56
+ }
57
+
58
+ const links = Array.isArray(value) ? value : [value];
59
+
60
+ return links.map((link) => parseWikiLink(link)).filter((path): path is string => path !== null);
61
+ }
62
+
63
+ /**
64
+ * Formats a file path as an Obsidian wiki link with display name.
65
+ * Example: "Projects/MyProject" -> "[[Projects/MyProject|MyProject]]"
66
+ *
67
+ * @param filePath - The file path to format
68
+ * @returns The formatted wiki link with display name alias
69
+ */
70
+ export function formatWikiLink(filePath: string): string {
71
+ if (!filePath || typeof filePath !== "string") {
72
+ return "";
73
+ }
74
+
75
+ const trimmed = filePath.trim();
76
+
77
+ if (!trimmed) {
78
+ return "";
79
+ }
80
+
81
+ // Extract the filename (last segment after the last /)
82
+ const segments = trimmed.split("/");
83
+ const displayName = segments[segments.length - 1];
84
+
85
+ // If there's no path separator, just return simple link
86
+ if (segments.length === 1) {
87
+ return `[[${trimmed}]]`;
88
+ }
89
+
90
+ return `[[${trimmed}|${displayName}]]`;
91
+ }