@real1ty-obsidian-plugins/utils 2.11.0 → 2.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.
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/property-renderer.d.ts +9 -0
- package/dist/core/property-renderer.d.ts.map +1 -0
- package/dist/core/property-renderer.js +42 -0
- package/dist/core/property-renderer.js.map +1 -0
- package/dist/file/file-utils.d.ts +28 -0
- package/dist/file/file-utils.d.ts.map +1 -0
- package/dist/file/file-utils.js +55 -0
- package/dist/file/file-utils.js.map +1 -0
- package/dist/file/file.d.ts +51 -1
- package/dist/file/file.d.ts.map +1 -1
- package/dist/file/file.js +69 -10
- package/dist/file/file.js.map +1 -1
- package/dist/file/index.d.ts +1 -0
- package/dist/file/index.d.ts.map +1 -1
- package/dist/file/index.js +1 -0
- package/dist/file/index.js.map +1 -1
- package/dist/file/link-parser.d.ts +28 -0
- package/dist/file/link-parser.d.ts.map +1 -1
- package/dist/file/link-parser.js +59 -0
- package/dist/file/link-parser.js.map +1 -1
- package/dist/string/filename-utils.d.ts +46 -0
- package/dist/string/filename-utils.d.ts.map +1 -0
- package/dist/string/filename-utils.js +65 -0
- package/dist/string/filename-utils.js.map +1 -0
- package/dist/string/index.d.ts +1 -0
- package/dist/string/index.d.ts.map +1 -1
- package/dist/string/index.js +1 -0
- package/dist/string/index.js.map +1 -1
- package/package.json +1 -1
- package/src/core/index.ts +1 -0
- package/src/core/property-renderer.ts +62 -0
- package/src/file/file-utils.ts +67 -0
- package/src/file/file.ts +90 -8
- package/src/file/index.ts +1 -0
- package/src/file/link-parser.ts +71 -0
- package/src/string/filename-utils.ts +77 -0
- package/src/string/index.ts +1 -0
package/dist/file/link-parser.js
CHANGED
|
@@ -75,4 +75,63 @@ export function formatWikiLink(filePath) {
|
|
|
75
75
|
}
|
|
76
76
|
return `[[${trimmed}|${displayName}]]`;
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Checks if a value is an Obsidian internal link in the format [[...]]
|
|
80
|
+
*/
|
|
81
|
+
export function isObsidianLink(value) {
|
|
82
|
+
if (typeof value !== "string")
|
|
83
|
+
return false;
|
|
84
|
+
const trimmed = value.trim();
|
|
85
|
+
return /^\[\[.+\]\]$/.test(trimmed);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Parses an Obsidian internal link and extracts its components
|
|
89
|
+
*
|
|
90
|
+
* Supports both formats:
|
|
91
|
+
* - Simple: [[Page Name]]
|
|
92
|
+
* - With alias: [[Path/To/Page|Display Name]]
|
|
93
|
+
*/
|
|
94
|
+
export function parseObsidianLink(linkString) {
|
|
95
|
+
var _a;
|
|
96
|
+
if (!isObsidianLink(linkString))
|
|
97
|
+
return null;
|
|
98
|
+
const trimmed = linkString.trim();
|
|
99
|
+
const linkContent = (_a = trimmed.match(/^\[\[(.+?)\]\]$/)) === null || _a === void 0 ? void 0 : _a[1];
|
|
100
|
+
if (!linkContent)
|
|
101
|
+
return null;
|
|
102
|
+
// Handle pipe syntax: [[path|display]]
|
|
103
|
+
if (linkContent.includes("|")) {
|
|
104
|
+
const parts = linkContent.split("|");
|
|
105
|
+
const path = parts[0].trim();
|
|
106
|
+
const alias = parts.slice(1).join("|").trim(); // Handle multiple pipes
|
|
107
|
+
return {
|
|
108
|
+
raw: trimmed,
|
|
109
|
+
path,
|
|
110
|
+
alias,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Simple format: [[path]]
|
|
114
|
+
const path = linkContent.trim();
|
|
115
|
+
return {
|
|
116
|
+
raw: trimmed,
|
|
117
|
+
path,
|
|
118
|
+
alias: path,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Gets the display alias from an Obsidian link
|
|
123
|
+
*/
|
|
124
|
+
export function getObsidianLinkAlias(linkString) {
|
|
125
|
+
var _a;
|
|
126
|
+
const parsed = parseObsidianLink(linkString);
|
|
127
|
+
return (_a = parsed === null || parsed === void 0 ? void 0 : parsed.alias) !== null && _a !== void 0 ? _a : linkString;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Gets the file path from an Obsidian link
|
|
131
|
+
*/
|
|
132
|
+
export function getObsidianLinkPath(linkString) {
|
|
133
|
+
var _a;
|
|
134
|
+
const parsed = parseObsidianLink(linkString);
|
|
135
|
+
return (_a = parsed === null || parsed === void 0 ? void 0 : parsed.path) !== null && _a !== void 0 ? _a : linkString;
|
|
136
|
+
}
|
|
78
137
|
//# sourceMappingURL=link-parser.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"link-parser.js","sourceRoot":"","sources":["../../src/file/link-parser.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,IAAY,EAAiB,EAAE;IACtE,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC;IACzD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAExB,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACjC,IAAI,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IAE3B,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAEpE,OAAO,QAAQ,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,KAAK,CAAC;AAC/D,CAAC,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACzC,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QACvC,OAAO,IAAI,CAAC;IACb,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAE5B,0DAA0D;IAC1D,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;IAEhE,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,OAAO,IAAI,CAAC;IACb,CAAC;IAED,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AACxB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAoC;IACtE,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,OAAO,EAAE,CAAC;IACX,CAAC;IAED,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAErD,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAkB,EAAE,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC;AACjG,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,QAAgB;IAC9C,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAC/C,OAAO,EAAE,CAAC;IACX,CAAC;IAED,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC;IAEhC,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,OAAO,EAAE,CAAC;IACX,CAAC;IAED,uDAAuD;IACvD,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACpC,MAAM,WAAW,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAElD,wDAAwD;IACxD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,KAAK,OAAO,IAAI,CAAC;IACzB,CAAC;IAED,OAAO,KAAK,OAAO,IAAI,WAAW,IAAI,CAAC;AACxC,CAAC","sourcesContent":["/**\n * Handles different link formats and edge cases:\n * [[FileName]] -> FileName.md\n * [[Folder/FileName]] -> Folder/FileName.md\n * [[Folder/FileName|DisplayName]] -> Folder/FileName.md\n * Normalizes paths and handles malformed links gracefully.\n */\nexport const extractFilePathFromLink = (link: string): string | null => {\n\tconst match = link.match(/\\[\\[([^|\\]]+?)(?:\\|.*?)?\\]\\]/);\n\tif (!match) return null;\n\n\tconst filePath = match[1].trim();\n\tif (!filePath) return null;\n\n\tif (filePath.includes(\"[[\") || filePath.includes(\"]]\")) return null;\n\n\treturn filePath.endsWith(\".md\") ? filePath : `${filePath}.md`;\n};\n\n/**\n * Parses an Obsidian wiki link to extract the file path.\n * Handles formats like:\n * - [[path/to/file]]\n * - [[path/to/file|Display Name]]\n *\n * @param link - The wiki link string\n * @returns The file path without the [[ ]] brackets, or null if invalid\n */\nexport function parseWikiLink(link: string): string | null {\n\tif (!link || typeof link !== \"string\") {\n\t\treturn null;\n\t}\n\n\tconst trimmed = link.trim();\n\n\t// Match [[path/to/file]] or [[path/to/file|Display Name]]\n\tconst match = trimmed.match(/^\\[\\[([^\\]|]+)(?:\\|[^\\]]+)?\\]\\]$/);\n\n\tif (!match) {\n\t\treturn null;\n\t}\n\n\treturn match[1].trim();\n}\n\n/**\n * Parses a property value that can be a single link or an array of links.\n * Extracts file paths from all valid wiki links.\n *\n * @param value - The property value (string, string[], or undefined)\n * @returns Array of file paths, empty if no valid links found\n */\nexport function parsePropertyLinks(value: string | string[] | undefined): string[] {\n\tif (!value) {\n\t\treturn [];\n\t}\n\n\tconst links = Array.isArray(value) ? value : [value];\n\n\treturn links.map((link) => parseWikiLink(link)).filter((path): path is string => path !== null);\n}\n\n/**\n * Formats a file path as an Obsidian wiki link with display name.\n * Example: \"Projects/MyProject\" -> \"[[Projects/MyProject|MyProject]]\"\n *\n * @param filePath - The file path to format\n * @returns The formatted wiki link with display name alias\n */\nexport function formatWikiLink(filePath: string): string {\n\tif (!filePath || typeof filePath !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\tconst trimmed = filePath.trim();\n\n\tif (!trimmed) {\n\t\treturn \"\";\n\t}\n\n\t// Extract the filename (last segment after the last /)\n\tconst segments = trimmed.split(\"/\");\n\tconst displayName = segments[segments.length - 1];\n\n\t// If there's no path separator, just return simple link\n\tif (segments.length === 1) {\n\t\treturn `[[${trimmed}]]`;\n\t}\n\n\treturn `[[${trimmed}|${displayName}]]`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"link-parser.js","sourceRoot":"","sources":["../../src/file/link-parser.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Handles different link formats and edge cases:\n * [[FileName]] -> FileName.md\n * [[Folder/FileName]] -> Folder/FileName.md\n * [[Folder/FileName|DisplayName]] -> Folder/FileName.md\n * Normalizes paths and handles malformed links gracefully.\n */\nexport const extractFilePathFromLink = (link: string): string | null => {\n\tconst match = link.match(/\\[\\[([^|\\]]+?)(?:\\|.*?)?\\]\\]/);\n\tif (!match) return null;\n\n\tconst filePath = match[1].trim();\n\tif (!filePath) return null;\n\n\tif (filePath.includes(\"[[\") || filePath.includes(\"]]\")) return null;\n\n\treturn filePath.endsWith(\".md\") ? filePath : `${filePath}.md`;\n};\n\n/**\n * Parses an Obsidian wiki link to extract the file path.\n * Handles formats like:\n * - [[path/to/file]]\n * - [[path/to/file|Display Name]]\n *\n * @param link - The wiki link string\n * @returns The file path without the [[ ]] brackets, or null if invalid\n */\nexport function parseWikiLink(link: string): string | null {\n\tif (!link || typeof link !== \"string\") {\n\t\treturn null;\n\t}\n\n\tconst trimmed = link.trim();\n\n\t// Match [[path/to/file]] or [[path/to/file|Display Name]]\n\tconst match = trimmed.match(/^\\[\\[([^\\]|]+)(?:\\|[^\\]]+)?\\]\\]$/);\n\n\tif (!match) {\n\t\treturn null;\n\t}\n\n\treturn match[1].trim();\n}\n\n/**\n * Parses a property value that can be a single link or an array of links.\n * Extracts file paths from all valid wiki links.\n *\n * @param value - The property value (string, string[], or undefined)\n * @returns Array of file paths, empty if no valid links found\n */\nexport function parsePropertyLinks(value: string | string[] | undefined): string[] {\n\tif (!value) {\n\t\treturn [];\n\t}\n\n\tconst links = Array.isArray(value) ? value : [value];\n\n\treturn links.map((link) => parseWikiLink(link)).filter((path): path is string => path !== null);\n}\n\n/**\n * Formats a file path as an Obsidian wiki link with display name.\n * Example: \"Projects/MyProject\" -> \"[[Projects/MyProject|MyProject]]\"\n *\n * @param filePath - The file path to format\n * @returns The formatted wiki link with display name alias\n */\nexport function formatWikiLink(filePath: string): string {\n\tif (!filePath || typeof filePath !== \"string\") {\n\t\treturn \"\";\n\t}\n\n\tconst trimmed = filePath.trim();\n\n\tif (!trimmed) {\n\t\treturn \"\";\n\t}\n\n\t// Extract the filename (last segment after the last /)\n\tconst segments = trimmed.split(\"/\");\n\tconst displayName = segments[segments.length - 1];\n\n\t// If there's no path separator, just return simple link\n\tif (segments.length === 1) {\n\t\treturn `[[${trimmed}]]`;\n\t}\n\n\treturn `[[${trimmed}|${displayName}]]`;\n}\n\n/**\n * Represents a parsed Obsidian link with its components\n */\nexport interface ObsidianLink {\n\traw: string;\n\tpath: string;\n\talias: string;\n}\n\n/**\n * Checks if a value is an Obsidian internal link in the format [[...]]\n */\nexport function isObsidianLink(value: unknown): boolean {\n\tif (typeof value !== \"string\") return false;\n\tconst trimmed = value.trim();\n\treturn /^\\[\\[.+\\]\\]$/.test(trimmed);\n}\n\n/**\n * Parses an Obsidian internal link and extracts its components\n *\n * Supports both formats:\n * - Simple: [[Page Name]]\n * - With alias: [[Path/To/Page|Display Name]]\n */\nexport function parseObsidianLink(linkString: string): ObsidianLink | null {\n\tif (!isObsidianLink(linkString)) return null;\n\n\tconst trimmed = linkString.trim();\n\tconst linkContent = trimmed.match(/^\\[\\[(.+?)\\]\\]$/)?.[1];\n\n\tif (!linkContent) return null;\n\n\t// Handle pipe syntax: [[path|display]]\n\tif (linkContent.includes(\"|\")) {\n\t\tconst parts = linkContent.split(\"|\");\n\t\tconst path = parts[0].trim();\n\t\tconst alias = parts.slice(1).join(\"|\").trim(); // Handle multiple pipes\n\n\t\treturn {\n\t\t\traw: trimmed,\n\t\t\tpath,\n\t\t\talias,\n\t\t};\n\t}\n\n\t// Simple format: [[path]]\n\tconst path = linkContent.trim();\n\treturn {\n\t\traw: trimmed,\n\t\tpath,\n\t\talias: path,\n\t};\n}\n\n/**\n * Gets the display alias from an Obsidian link\n */\nexport function getObsidianLinkAlias(linkString: string): string {\n\tconst parsed = parseObsidianLink(linkString);\n\treturn parsed?.alias ?? linkString;\n}\n\n/**\n * Gets the file path from an Obsidian link\n */\nexport function getObsidianLinkPath(linkString: string): string {\n\tconst parsed = parseObsidianLink(linkString);\n\treturn parsed?.path ?? linkString;\n}\n"]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes a directory path for consistent comparison.
|
|
3
|
+
*
|
|
4
|
+
* - Trims whitespace
|
|
5
|
+
* - Removes leading and trailing slashes
|
|
6
|
+
* - Converts empty/whitespace-only strings to empty string
|
|
7
|
+
*
|
|
8
|
+
* Examples:
|
|
9
|
+
* - "tasks/" → "tasks"
|
|
10
|
+
* - "/tasks" → "tasks"
|
|
11
|
+
* - "/tasks/" → "tasks"
|
|
12
|
+
* - " tasks " → "tasks"
|
|
13
|
+
* - "" → ""
|
|
14
|
+
* - " " → ""
|
|
15
|
+
* - "tasks/homework" → "tasks/homework"
|
|
16
|
+
*/
|
|
17
|
+
export declare const normalizeDirectoryPath: (directory: string) => string;
|
|
18
|
+
/**
|
|
19
|
+
* Extracts the date and suffix (everything after the date) from a physical instance filename.
|
|
20
|
+
* Physical instance format: "[title] [date]-[ZETTELID]"
|
|
21
|
+
*
|
|
22
|
+
* @param basename - The filename without extension
|
|
23
|
+
* @returns Object with dateStr and suffix, or null if no date found
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* extractDateAndSuffix("My Event 2025-01-15-ABC123") // { dateStr: "2025-01-15", suffix: "-ABC123" }
|
|
27
|
+
* extractDateAndSuffix("Invalid filename") // null
|
|
28
|
+
*/
|
|
29
|
+
export declare const extractDateAndSuffix: (basename: string) => {
|
|
30
|
+
dateStr: string;
|
|
31
|
+
suffix: string;
|
|
32
|
+
} | null;
|
|
33
|
+
/**
|
|
34
|
+
* Rebuilds a physical instance filename with a new title while preserving the date and zettel ID.
|
|
35
|
+
* Physical instance format: "[title] [date]-[ZETTELID]"
|
|
36
|
+
*
|
|
37
|
+
* @param currentBasename - Current filename without extension
|
|
38
|
+
* @param newTitle - New title (with or without zettel ID - will be stripped)
|
|
39
|
+
* @returns New filename, or null if current filename format is invalid
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* rebuildPhysicalInstanceFilename("Old Title 2025-01-15-ABC123", "New Title-XYZ789")
|
|
43
|
+
* // Returns: "New Title 2025-01-15-ABC123"
|
|
44
|
+
*/
|
|
45
|
+
export declare const rebuildPhysicalInstanceFilename: (currentBasename: string, newTitle: string) => string | null;
|
|
46
|
+
//# sourceMappingURL=filename-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filename-utils.d.ts","sourceRoot":"","sources":["../../src/string/filename-utils.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,sBAAsB,GAAI,WAAW,MAAM,KAAG,MAE1D,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,GAChC,UAAU,MAAM,KACd;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,IAWxC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,+BAA+B,GAC3C,iBAAiB,MAAM,EACvB,UAAU,MAAM,KACd,MAAM,GAAG,IAaX,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { sanitizeFilenamePreserveSpaces } from "../file/file";
|
|
2
|
+
/**
|
|
3
|
+
* Normalizes a directory path for consistent comparison.
|
|
4
|
+
*
|
|
5
|
+
* - Trims whitespace
|
|
6
|
+
* - Removes leading and trailing slashes
|
|
7
|
+
* - Converts empty/whitespace-only strings to empty string
|
|
8
|
+
*
|
|
9
|
+
* Examples:
|
|
10
|
+
* - "tasks/" → "tasks"
|
|
11
|
+
* - "/tasks" → "tasks"
|
|
12
|
+
* - "/tasks/" → "tasks"
|
|
13
|
+
* - " tasks " → "tasks"
|
|
14
|
+
* - "" → ""
|
|
15
|
+
* - " " → ""
|
|
16
|
+
* - "tasks/homework" → "tasks/homework"
|
|
17
|
+
*/
|
|
18
|
+
export const normalizeDirectoryPath = (directory) => {
|
|
19
|
+
return directory.trim().replace(/^\/+|\/+$/g, "");
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Extracts the date and suffix (everything after the date) from a physical instance filename.
|
|
23
|
+
* Physical instance format: "[title] [date]-[ZETTELID]"
|
|
24
|
+
*
|
|
25
|
+
* @param basename - The filename without extension
|
|
26
|
+
* @returns Object with dateStr and suffix, or null if no date found
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* extractDateAndSuffix("My Event 2025-01-15-ABC123") // { dateStr: "2025-01-15", suffix: "-ABC123" }
|
|
30
|
+
* extractDateAndSuffix("Invalid filename") // null
|
|
31
|
+
*/
|
|
32
|
+
export const extractDateAndSuffix = (basename) => {
|
|
33
|
+
const dateMatch = basename.match(/(\d{4}-\d{2}-\d{2})/);
|
|
34
|
+
if (!dateMatch) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const dateStr = dateMatch[1];
|
|
38
|
+
const dateIndex = basename.indexOf(dateStr);
|
|
39
|
+
const suffix = basename.substring(dateIndex + dateStr.length);
|
|
40
|
+
return { dateStr, suffix };
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Rebuilds a physical instance filename with a new title while preserving the date and zettel ID.
|
|
44
|
+
* Physical instance format: "[title] [date]-[ZETTELID]"
|
|
45
|
+
*
|
|
46
|
+
* @param currentBasename - Current filename without extension
|
|
47
|
+
* @param newTitle - New title (with or without zettel ID - will be stripped)
|
|
48
|
+
* @returns New filename, or null if current filename format is invalid
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* rebuildPhysicalInstanceFilename("Old Title 2025-01-15-ABC123", "New Title-XYZ789")
|
|
52
|
+
* // Returns: "New Title 2025-01-15-ABC123"
|
|
53
|
+
*/
|
|
54
|
+
export const rebuildPhysicalInstanceFilename = (currentBasename, newTitle) => {
|
|
55
|
+
const dateAndSuffix = extractDateAndSuffix(currentBasename);
|
|
56
|
+
if (!dateAndSuffix) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const { dateStr, suffix } = dateAndSuffix;
|
|
60
|
+
// Remove any zettel ID from the new title (just in case)
|
|
61
|
+
const newTitleClean = newTitle.replace(/-[A-Z0-9]{6}$/, "");
|
|
62
|
+
const newTitleSanitized = sanitizeFilenamePreserveSpaces(newTitleClean);
|
|
63
|
+
return `${newTitleSanitized} ${dateStr}${suffix}`;
|
|
64
|
+
};
|
|
65
|
+
//# sourceMappingURL=filename-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filename-utils.js","sourceRoot":"","sources":["../../src/string/filename-utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,8BAA8B,EAAE,MAAM,cAAc,CAAC;AAE9D;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,SAAiB,EAAU,EAAE;IACnE,OAAO,SAAS,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,YAAY,EAAE,EAAE,CAAC,CAAC;AACnD,CAAC,CAAC;AAEF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CACnC,QAAgB,EAC6B,EAAE;IAC/C,MAAM,SAAS,GAAG,QAAQ,CAAC,KAAK,CAAC,qBAAqB,CAAC,CAAC;IACxD,IAAI,CAAC,SAAS,EAAE,CAAC;QAChB,OAAO,IAAI,CAAC;IACb,CAAC;IAED,MAAM,OAAO,GAAG,SAAS,CAAC,CAAC,CAAC,CAAC;IAC7B,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,QAAQ,CAAC,SAAS,CAAC,SAAS,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAE9D,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;AAC5B,CAAC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG,CAC9C,eAAuB,EACvB,QAAgB,EACA,EAAE;IAClB,MAAM,aAAa,GAAG,oBAAoB,CAAC,eAAe,CAAC,CAAC;IAC5D,IAAI,CAAC,aAAa,EAAE,CAAC;QACpB,OAAO,IAAI,CAAC;IACb,CAAC;IAED,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,aAAa,CAAC;IAE1C,yDAAyD;IACzD,MAAM,aAAa,GAAG,QAAQ,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;IAC5D,MAAM,iBAAiB,GAAG,8BAA8B,CAAC,aAAa,CAAC,CAAC;IAExE,OAAO,GAAG,iBAAiB,IAAI,OAAO,GAAG,MAAM,EAAE,CAAC;AACnD,CAAC,CAAC","sourcesContent":["import { sanitizeFilenamePreserveSpaces } from \"../file/file\";\n\n/**\n * Normalizes a directory path for consistent comparison.\n *\n * - Trims whitespace\n * - Removes leading and trailing slashes\n * - Converts empty/whitespace-only strings to empty string\n *\n * Examples:\n * - \"tasks/\" → \"tasks\"\n * - \"/tasks\" → \"tasks\"\n * - \"/tasks/\" → \"tasks\"\n * - \" tasks \" → \"tasks\"\n * - \"\" → \"\"\n * - \" \" → \"\"\n * - \"tasks/homework\" → \"tasks/homework\"\n */\nexport const normalizeDirectoryPath = (directory: string): string => {\n\treturn directory.trim().replace(/^\\/+|\\/+$/g, \"\");\n};\n\n/**\n * Extracts the date and suffix (everything after the date) from a physical instance filename.\n * Physical instance format: \"[title] [date]-[ZETTELID]\"\n *\n * @param basename - The filename without extension\n * @returns Object with dateStr and suffix, or null if no date found\n *\n * @example\n * extractDateAndSuffix(\"My Event 2025-01-15-ABC123\") // { dateStr: \"2025-01-15\", suffix: \"-ABC123\" }\n * extractDateAndSuffix(\"Invalid filename\") // null\n */\nexport const extractDateAndSuffix = (\n\tbasename: string\n): { dateStr: string; suffix: string } | null => {\n\tconst dateMatch = basename.match(/(\\d{4}-\\d{2}-\\d{2})/);\n\tif (!dateMatch) {\n\t\treturn null;\n\t}\n\n\tconst dateStr = dateMatch[1];\n\tconst dateIndex = basename.indexOf(dateStr);\n\tconst suffix = basename.substring(dateIndex + dateStr.length);\n\n\treturn { dateStr, suffix };\n};\n\n/**\n * Rebuilds a physical instance filename with a new title while preserving the date and zettel ID.\n * Physical instance format: \"[title] [date]-[ZETTELID]\"\n *\n * @param currentBasename - Current filename without extension\n * @param newTitle - New title (with or without zettel ID - will be stripped)\n * @returns New filename, or null if current filename format is invalid\n *\n * @example\n * rebuildPhysicalInstanceFilename(\"Old Title 2025-01-15-ABC123\", \"New Title-XYZ789\")\n * // Returns: \"New Title 2025-01-15-ABC123\"\n */\nexport const rebuildPhysicalInstanceFilename = (\n\tcurrentBasename: string,\n\tnewTitle: string\n): string | null => {\n\tconst dateAndSuffix = extractDateAndSuffix(currentBasename);\n\tif (!dateAndSuffix) {\n\t\treturn null;\n\t}\n\n\tconst { dateStr, suffix } = dateAndSuffix;\n\n\t// Remove any zettel ID from the new title (just in case)\n\tconst newTitleClean = newTitle.replace(/-[A-Z0-9]{6}$/, \"\");\n\tconst newTitleSanitized = sanitizeFilenamePreserveSpaces(newTitleClean);\n\n\treturn `${newTitleSanitized} ${dateStr}${suffix}`;\n};\n"]}
|
package/dist/string/index.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/string/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/string/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,UAAU,CAAC"}
|
package/dist/string/index.js
CHANGED
package/dist/string/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/string/index.ts"],"names":[],"mappings":"AAAA,cAAc,UAAU,CAAC","sourcesContent":["export * from \"./string\";\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/string/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,UAAU,CAAC","sourcesContent":["export * from \"./filename-utils\";\nexport * from \"./string\";\n"]}
|
package/package.json
CHANGED
package/src/core/index.ts
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { getObsidianLinkAlias, getObsidianLinkPath, isObsidianLink } from "../file/link-parser";
|
|
2
|
+
|
|
3
|
+
export interface PropertyRendererConfig {
|
|
4
|
+
createLink: (text: string, path: string, isObsidianLink: boolean) => HTMLElement;
|
|
5
|
+
createText: (text: string) => HTMLElement | Text;
|
|
6
|
+
createSeparator?: () => HTMLElement | Text;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function renderPropertyValue(
|
|
10
|
+
container: HTMLElement,
|
|
11
|
+
value: any,
|
|
12
|
+
config: PropertyRendererConfig
|
|
13
|
+
): void {
|
|
14
|
+
// Handle arrays - render each item separately
|
|
15
|
+
if (Array.isArray(value)) {
|
|
16
|
+
const hasClickableLinks = value.some(isObsidianLink);
|
|
17
|
+
|
|
18
|
+
if (hasClickableLinks) {
|
|
19
|
+
for (let index = 0; index < value.length; index++) {
|
|
20
|
+
if (index > 0 && config.createSeparator) {
|
|
21
|
+
container.appendChild(config.createSeparator());
|
|
22
|
+
}
|
|
23
|
+
renderSingleValue(container, value[index], config);
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
// Plain array - just join with commas
|
|
27
|
+
const textNode = config.createText(value.join(", "));
|
|
28
|
+
container.appendChild(textNode);
|
|
29
|
+
}
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
renderSingleValue(container, value, config);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function renderSingleValue(
|
|
37
|
+
container: HTMLElement,
|
|
38
|
+
value: any,
|
|
39
|
+
config: PropertyRendererConfig
|
|
40
|
+
): void {
|
|
41
|
+
const stringValue = String(value).trim();
|
|
42
|
+
|
|
43
|
+
if (isObsidianLink(stringValue)) {
|
|
44
|
+
const displayText = getObsidianLinkAlias(stringValue);
|
|
45
|
+
const linkPath = getObsidianLinkPath(stringValue);
|
|
46
|
+
const link = config.createLink(displayText, linkPath, true);
|
|
47
|
+
container.appendChild(link);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Regular text
|
|
52
|
+
const textNode = config.createText(stringValue);
|
|
53
|
+
container.appendChild(textNode);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createTextNode(text: string): Text {
|
|
57
|
+
return document.createTextNode(text);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createDefaultSeparator(): Text {
|
|
61
|
+
return document.createTextNode(", ");
|
|
62
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { App } from "obsidian";
|
|
2
|
+
import { TFile } from "obsidian";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Gets a TFile by path or throws an error if not found.
|
|
6
|
+
* Useful when you need to ensure a file exists before proceeding.
|
|
7
|
+
*/
|
|
8
|
+
export const getTFileOrThrow = (app: App, path: string): TFile => {
|
|
9
|
+
const f = app.vault.getAbstractFileByPath(path);
|
|
10
|
+
if (!(f instanceof TFile)) throw new Error(`File not found: ${path}`);
|
|
11
|
+
return f;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Executes an operation on a file's frontmatter.
|
|
16
|
+
* Wrapper around Obsidian's processFrontMatter for more concise usage.
|
|
17
|
+
*/
|
|
18
|
+
export const withFrontmatter = async (
|
|
19
|
+
app: App,
|
|
20
|
+
file: TFile,
|
|
21
|
+
update: (fm: Record<string, unknown>) => void
|
|
22
|
+
) => app.fileManager.processFrontMatter(file, update);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Creates a backup copy of a file's frontmatter.
|
|
26
|
+
* Useful for undo/redo operations or temporary modifications.
|
|
27
|
+
*/
|
|
28
|
+
export const backupFrontmatter = async (app: App, file: TFile) => {
|
|
29
|
+
let copy: Record<string, unknown> = {};
|
|
30
|
+
await withFrontmatter(app, file, (fm) => {
|
|
31
|
+
copy = { ...fm };
|
|
32
|
+
});
|
|
33
|
+
return copy;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Restores a file's frontmatter from a backup.
|
|
38
|
+
* Clears existing frontmatter and replaces with the backup.
|
|
39
|
+
*/
|
|
40
|
+
export const restoreFrontmatter = async (
|
|
41
|
+
app: App,
|
|
42
|
+
file: TFile,
|
|
43
|
+
original: Record<string, unknown>
|
|
44
|
+
) =>
|
|
45
|
+
withFrontmatter(app, file, (fm) => {
|
|
46
|
+
for (const k of Object.keys(fm)) {
|
|
47
|
+
delete fm[k];
|
|
48
|
+
}
|
|
49
|
+
Object.assign(fm, original);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extracts the content that appears after the frontmatter section.
|
|
54
|
+
* Returns the entire content if no frontmatter is found.
|
|
55
|
+
*/
|
|
56
|
+
export const extractContentAfterFrontmatter = (fullContent: string): string => {
|
|
57
|
+
const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n/;
|
|
58
|
+
const match = fullContent.match(frontmatterRegex);
|
|
59
|
+
|
|
60
|
+
if (match) {
|
|
61
|
+
// Return content after frontmatter
|
|
62
|
+
return fullContent.substring(match.index! + match[0].length);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// If no frontmatter found, return the entire content
|
|
66
|
+
return fullContent;
|
|
67
|
+
};
|
package/src/file/file.ts
CHANGED
|
@@ -548,16 +548,98 @@ export function findRootNodesInFolder(app: App, folderPath: string): string[] {
|
|
|
548
548
|
}
|
|
549
549
|
|
|
550
550
|
// ============================================================================
|
|
551
|
-
//
|
|
551
|
+
// Filename Sanitization
|
|
552
552
|
// ============================================================================
|
|
553
553
|
|
|
554
|
-
export
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
554
|
+
export interface SanitizeFilenameOptions {
|
|
555
|
+
/**
|
|
556
|
+
* Style of sanitization to apply.
|
|
557
|
+
* - "kebab": Convert to lowercase, replace spaces with hyphens (default, backwards compatible)
|
|
558
|
+
* - "preserve": Preserve spaces and case, only remove invalid characters
|
|
559
|
+
*/
|
|
560
|
+
style?: "kebab" | "preserve";
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Sanitizes a string for use as a filename.
|
|
565
|
+
* Defaults to kebab-case style for backwards compatibility.
|
|
566
|
+
*
|
|
567
|
+
* @param input - String to sanitize
|
|
568
|
+
* @param options - Sanitization options
|
|
569
|
+
* @returns Sanitized filename string
|
|
570
|
+
*
|
|
571
|
+
* @example
|
|
572
|
+
* // Default kebab-case style (backwards compatible)
|
|
573
|
+
* sanitizeForFilename("My File Name") // "my-file-name"
|
|
574
|
+
*
|
|
575
|
+
* // Preserve spaces and case
|
|
576
|
+
* sanitizeForFilename("My File Name", { style: "preserve" }) // "My File Name"
|
|
577
|
+
*/
|
|
578
|
+
export const sanitizeForFilename = (
|
|
579
|
+
input: string,
|
|
580
|
+
options: SanitizeFilenameOptions = {}
|
|
581
|
+
): string => {
|
|
582
|
+
const { style = "kebab" } = options;
|
|
583
|
+
|
|
584
|
+
if (style === "preserve") {
|
|
585
|
+
return sanitizeFilenamePreserveSpaces(input);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Default: kebab-case style (legacy behavior)
|
|
589
|
+
return sanitizeFilenameKebabCase(input);
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Sanitizes filename using kebab-case style.
|
|
594
|
+
* - Removes invalid characters
|
|
595
|
+
* - Converts to lowercase
|
|
596
|
+
* - Replaces spaces with hyphens
|
|
597
|
+
*
|
|
598
|
+
* Best for: CLI tools, URLs, slugs, technical files
|
|
599
|
+
*
|
|
600
|
+
* @example
|
|
601
|
+
* sanitizeFilenameKebabCase("My File Name") // "my-file-name"
|
|
602
|
+
* sanitizeFilenameKebabCase("Travel Around The World") // "travel-around-the-world"
|
|
603
|
+
*/
|
|
604
|
+
export const sanitizeFilenameKebabCase = (input: string): string => {
|
|
605
|
+
return (
|
|
606
|
+
input
|
|
607
|
+
// Remove invalid filename characters
|
|
608
|
+
.replace(/[<>:"/\\|?*]/g, "")
|
|
609
|
+
// Replace spaces with hyphens
|
|
610
|
+
.replace(/\s+/g, "-")
|
|
611
|
+
// Replace multiple hyphens with single
|
|
612
|
+
.replace(/-+/g, "-")
|
|
613
|
+
// Remove leading/trailing hyphens
|
|
614
|
+
.replace(/^-|-$/g, "")
|
|
615
|
+
// Convert to lowercase
|
|
616
|
+
.toLowerCase()
|
|
617
|
+
);
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Sanitizes filename while preserving spaces and case.
|
|
622
|
+
* - Removes invalid characters only
|
|
623
|
+
* - Preserves spaces and original casing
|
|
624
|
+
* - Removes trailing dots (Windows compatibility)
|
|
625
|
+
*
|
|
626
|
+
* Best for: Note titles, human-readable filenames, Obsidian notes
|
|
627
|
+
*
|
|
628
|
+
* @example
|
|
629
|
+
* sanitizeFilenamePreserveSpaces("My File Name") // "My File Name"
|
|
630
|
+
* sanitizeFilenamePreserveSpaces("Travel Around The World") // "Travel Around The World"
|
|
631
|
+
* sanitizeFilenamePreserveSpaces("File<Invalid>Chars") // "FileInvalidChars"
|
|
632
|
+
*/
|
|
633
|
+
export const sanitizeFilenamePreserveSpaces = (input: string): string => {
|
|
634
|
+
return (
|
|
635
|
+
input
|
|
636
|
+
// Remove invalid filename characters (cross-platform compatibility)
|
|
637
|
+
.replace(/[<>:"/\\|?*]/g, "")
|
|
638
|
+
// Remove trailing dots (invalid on Windows)
|
|
639
|
+
.replace(/\.+$/g, "")
|
|
640
|
+
// Remove leading/trailing whitespace
|
|
641
|
+
.trim()
|
|
642
|
+
);
|
|
561
643
|
};
|
|
562
644
|
|
|
563
645
|
export const getFilenameFromPath = (filePath: string): string => {
|
package/src/file/index.ts
CHANGED
package/src/file/link-parser.ts
CHANGED
|
@@ -89,3 +89,74 @@ export function formatWikiLink(filePath: string): string {
|
|
|
89
89
|
|
|
90
90
|
return `[[${trimmed}|${displayName}]]`;
|
|
91
91
|
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Represents a parsed Obsidian link with its components
|
|
95
|
+
*/
|
|
96
|
+
export interface ObsidianLink {
|
|
97
|
+
raw: string;
|
|
98
|
+
path: string;
|
|
99
|
+
alias: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Checks if a value is an Obsidian internal link in the format [[...]]
|
|
104
|
+
*/
|
|
105
|
+
export function isObsidianLink(value: unknown): boolean {
|
|
106
|
+
if (typeof value !== "string") return false;
|
|
107
|
+
const trimmed = value.trim();
|
|
108
|
+
return /^\[\[.+\]\]$/.test(trimmed);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Parses an Obsidian internal link and extracts its components
|
|
113
|
+
*
|
|
114
|
+
* Supports both formats:
|
|
115
|
+
* - Simple: [[Page Name]]
|
|
116
|
+
* - With alias: [[Path/To/Page|Display Name]]
|
|
117
|
+
*/
|
|
118
|
+
export function parseObsidianLink(linkString: string): ObsidianLink | null {
|
|
119
|
+
if (!isObsidianLink(linkString)) return null;
|
|
120
|
+
|
|
121
|
+
const trimmed = linkString.trim();
|
|
122
|
+
const linkContent = trimmed.match(/^\[\[(.+?)\]\]$/)?.[1];
|
|
123
|
+
|
|
124
|
+
if (!linkContent) return null;
|
|
125
|
+
|
|
126
|
+
// Handle pipe syntax: [[path|display]]
|
|
127
|
+
if (linkContent.includes("|")) {
|
|
128
|
+
const parts = linkContent.split("|");
|
|
129
|
+
const path = parts[0].trim();
|
|
130
|
+
const alias = parts.slice(1).join("|").trim(); // Handle multiple pipes
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
raw: trimmed,
|
|
134
|
+
path,
|
|
135
|
+
alias,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Simple format: [[path]]
|
|
140
|
+
const path = linkContent.trim();
|
|
141
|
+
return {
|
|
142
|
+
raw: trimmed,
|
|
143
|
+
path,
|
|
144
|
+
alias: path,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Gets the display alias from an Obsidian link
|
|
150
|
+
*/
|
|
151
|
+
export function getObsidianLinkAlias(linkString: string): string {
|
|
152
|
+
const parsed = parseObsidianLink(linkString);
|
|
153
|
+
return parsed?.alias ?? linkString;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Gets the file path from an Obsidian link
|
|
158
|
+
*/
|
|
159
|
+
export function getObsidianLinkPath(linkString: string): string {
|
|
160
|
+
const parsed = parseObsidianLink(linkString);
|
|
161
|
+
return parsed?.path ?? linkString;
|
|
162
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { sanitizeFilenamePreserveSpaces } from "../file/file";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Normalizes a directory path for consistent comparison.
|
|
5
|
+
*
|
|
6
|
+
* - Trims whitespace
|
|
7
|
+
* - Removes leading and trailing slashes
|
|
8
|
+
* - Converts empty/whitespace-only strings to empty string
|
|
9
|
+
*
|
|
10
|
+
* Examples:
|
|
11
|
+
* - "tasks/" → "tasks"
|
|
12
|
+
* - "/tasks" → "tasks"
|
|
13
|
+
* - "/tasks/" → "tasks"
|
|
14
|
+
* - " tasks " → "tasks"
|
|
15
|
+
* - "" → ""
|
|
16
|
+
* - " " → ""
|
|
17
|
+
* - "tasks/homework" → "tasks/homework"
|
|
18
|
+
*/
|
|
19
|
+
export const normalizeDirectoryPath = (directory: string): string => {
|
|
20
|
+
return directory.trim().replace(/^\/+|\/+$/g, "");
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extracts the date and suffix (everything after the date) from a physical instance filename.
|
|
25
|
+
* Physical instance format: "[title] [date]-[ZETTELID]"
|
|
26
|
+
*
|
|
27
|
+
* @param basename - The filename without extension
|
|
28
|
+
* @returns Object with dateStr and suffix, or null if no date found
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* extractDateAndSuffix("My Event 2025-01-15-ABC123") // { dateStr: "2025-01-15", suffix: "-ABC123" }
|
|
32
|
+
* extractDateAndSuffix("Invalid filename") // null
|
|
33
|
+
*/
|
|
34
|
+
export const extractDateAndSuffix = (
|
|
35
|
+
basename: string
|
|
36
|
+
): { dateStr: string; suffix: string } | null => {
|
|
37
|
+
const dateMatch = basename.match(/(\d{4}-\d{2}-\d{2})/);
|
|
38
|
+
if (!dateMatch) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const dateStr = dateMatch[1];
|
|
43
|
+
const dateIndex = basename.indexOf(dateStr);
|
|
44
|
+
const suffix = basename.substring(dateIndex + dateStr.length);
|
|
45
|
+
|
|
46
|
+
return { dateStr, suffix };
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Rebuilds a physical instance filename with a new title while preserving the date and zettel ID.
|
|
51
|
+
* Physical instance format: "[title] [date]-[ZETTELID]"
|
|
52
|
+
*
|
|
53
|
+
* @param currentBasename - Current filename without extension
|
|
54
|
+
* @param newTitle - New title (with or without zettel ID - will be stripped)
|
|
55
|
+
* @returns New filename, or null if current filename format is invalid
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* rebuildPhysicalInstanceFilename("Old Title 2025-01-15-ABC123", "New Title-XYZ789")
|
|
59
|
+
* // Returns: "New Title 2025-01-15-ABC123"
|
|
60
|
+
*/
|
|
61
|
+
export const rebuildPhysicalInstanceFilename = (
|
|
62
|
+
currentBasename: string,
|
|
63
|
+
newTitle: string
|
|
64
|
+
): string | null => {
|
|
65
|
+
const dateAndSuffix = extractDateAndSuffix(currentBasename);
|
|
66
|
+
if (!dateAndSuffix) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { dateStr, suffix } = dateAndSuffix;
|
|
71
|
+
|
|
72
|
+
// Remove any zettel ID from the new title (just in case)
|
|
73
|
+
const newTitleClean = newTitle.replace(/-[A-Z0-9]{6}$/, "");
|
|
74
|
+
const newTitleSanitized = sanitizeFilenamePreserveSpaces(newTitleClean);
|
|
75
|
+
|
|
76
|
+
return `${newTitleSanitized} ${dateStr}${suffix}`;
|
|
77
|
+
};
|
package/src/string/index.ts
CHANGED