yanki 2.0.11 → 2.0.13
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/bin/cli.js +154 -154
- package/dist/lib/index.js +112 -86
- package/dist/standalone/index.js +83 -83
- package/package.json +6 -6
package/dist/lib/index.js
CHANGED
|
@@ -509,7 +509,7 @@ function getFileExtensionForMimeType(mimeType) {
|
|
|
509
509
|
//#endregion
|
|
510
510
|
//#region src/lib/utilities/path.ts
|
|
511
511
|
const WINDOWS_DRIVE_LETTER_REGEX = /* @__PURE__ */ new RegExp("^[A-Z]:", "iv");
|
|
512
|
-
const QUERY_FRAGMENT_START_REGEX = /* @__PURE__ */ new RegExp("[
|
|
512
|
+
const QUERY_FRAGMENT_START_REGEX = /* @__PURE__ */ new RegExp("[#^]", "v");
|
|
513
513
|
/**
|
|
514
514
|
* The browserify polyfill doesn't implement win32 absolute path detection...
|
|
515
515
|
*
|
|
@@ -575,26 +575,36 @@ function getBaseAndQueryParts(filePath) {
|
|
|
575
575
|
const [base, query] = splitAtFirstMatch(path.basename(filePath), QUERY_FRAGMENT_START_REGEX);
|
|
576
576
|
return [path.join(directoryPath, base), query];
|
|
577
577
|
}
|
|
578
|
+
/**
|
|
579
|
+
* Get every plausible base and anchor interpretation of a local link target
|
|
580
|
+
* that may contain anchor delimiter characters (`#`, `^`), ordered from the
|
|
581
|
+
* longest literal path (no anchor at all) to the shortest (anchor starts at the
|
|
582
|
+
* first delimiter, matching Obsidian's anchor syntax).
|
|
583
|
+
*
|
|
584
|
+
* This operates on the unnormalized link target and deliberately considers
|
|
585
|
+
* delimiters before slashes. A slash after a delimiter belongs to the anchor,
|
|
586
|
+
* not the file path. File and directory names may also legitimately contain
|
|
587
|
+
* these characters, so callers must normalize only each candidate's `filePath`
|
|
588
|
+
* and try candidates in order against a list of real files.
|
|
589
|
+
* https://github.com/kitschpatrol/yanki/issues/20
|
|
590
|
+
*/
|
|
591
|
+
function getLocalPathCandidates(linkTarget) {
|
|
592
|
+
const candidates = [{
|
|
593
|
+
anchor: void 0,
|
|
594
|
+
filePath: linkTarget
|
|
595
|
+
}];
|
|
596
|
+
for (let index = linkTarget.length - 1; index >= 0; index--) if (QUERY_FRAGMENT_START_REGEX.test(linkTarget.charAt(index))) candidates.push({
|
|
597
|
+
anchor: linkTarget.slice(index),
|
|
598
|
+
filePath: linkTarget.slice(0, index)
|
|
599
|
+
});
|
|
600
|
+
return candidates;
|
|
601
|
+
}
|
|
578
602
|
function getBase(filePath) {
|
|
579
603
|
return getBaseAndQueryParts(filePath)[0];
|
|
580
604
|
}
|
|
581
605
|
function getQuery(filePath) {
|
|
582
606
|
return getBaseAndQueryParts(filePath).at(1) ?? "";
|
|
583
607
|
}
|
|
584
|
-
function hasExtension(filePath) {
|
|
585
|
-
return getExtension(filePath) !== "";
|
|
586
|
-
}
|
|
587
|
-
function getExtension(filePath) {
|
|
588
|
-
return path.extname(getBase(filePath));
|
|
589
|
-
}
|
|
590
|
-
function addExtensionIfMissing(filePath, extension) {
|
|
591
|
-
if (hasExtension(filePath)) return filePath;
|
|
592
|
-
return addExtension(filePath, extension);
|
|
593
|
-
}
|
|
594
|
-
function addExtension(filePath, extension) {
|
|
595
|
-
const [base, query] = getBaseAndQueryParts(filePath);
|
|
596
|
-
return `${base}.${extension}${query ?? ""}`;
|
|
597
|
-
}
|
|
598
608
|
//#endregion
|
|
599
609
|
//#region src/lib/utilities/url.ts
|
|
600
610
|
const DRIVE_LETTER_REGEX = /* @__PURE__ */ new RegExp("^[a-z]:", "iv");
|
|
@@ -1792,7 +1802,9 @@ const defaultResolveLinkOptions = {
|
|
|
1792
1802
|
* @returns Resolved absolute path or URL One of:
|
|
1793
1803
|
*
|
|
1794
1804
|
* - Resolved absolute POSIX-style paths
|
|
1795
|
-
* - Removes any
|
|
1805
|
+
* - Removes any Obsidian-style heading and block anchors, unless the anchor
|
|
1806
|
+
* characters are a literal part of a matched file's name (`?` is always
|
|
1807
|
+
* treated as a literal file name character in local paths)
|
|
1796
1808
|
* - Not URI-encoded
|
|
1797
1809
|
* - Retains original case
|
|
1798
1810
|
* - HTTP protocol URL
|
|
@@ -1800,49 +1812,25 @@ const defaultResolveLinkOptions = {
|
|
|
1800
1812
|
* parameters)
|
|
1801
1813
|
*/
|
|
1802
1814
|
function resolveLink(filePathOrUrl, options) {
|
|
1803
|
-
const
|
|
1815
|
+
const resolvedOptions = deepmerge(defaultResolveLinkOptions, options ?? {});
|
|
1816
|
+
const { allFilePaths, convertFilePathsToProtocol, cwd, obsidianVaultName } = resolvedOptions;
|
|
1804
1817
|
if (convertFilePathsToProtocol === "obsidian" && obsidianVaultName === void 0) console.warn(`convertFilePathsToProtocol is 'obsidian', but no obsidianVaultName provided`);
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
let resolvedUrl = addExtensionIfMissing(normalize(decodedUrl), "md");
|
|
1811
|
-
resolvedUrl = resolveNameLink(resolvedUrl, cwd, allFilePaths ?? []) ?? resolveWithBasePath(decodedUrl, {
|
|
1812
|
-
basePath,
|
|
1813
|
-
cwd
|
|
1814
|
-
});
|
|
1815
|
-
if (getSrcType(resolvedUrl) === "localFilePath") {
|
|
1816
|
-
currentPathOrUrl = resolvedUrl;
|
|
1817
|
-
continue;
|
|
1818
|
-
}
|
|
1819
|
-
console.warn(`Failed to convert local file wiki-style name to path: ${currentPathOrUrl} --> ${resolvedUrl}`);
|
|
1820
|
-
return resolvedUrl;
|
|
1818
|
+
const decodedUrl = safeDecodeURI(filePathOrUrl) ?? filePathOrUrl;
|
|
1819
|
+
switch (getSrcType(decodedUrl)) {
|
|
1820
|
+
case "localFileName": {
|
|
1821
|
+
const resolvedNameLink = resolveNameLink(decodedUrl, cwd, allFilePaths ?? []);
|
|
1822
|
+
return resolvedNameLink === void 0 ? resolveLocalFilePath(decodedUrl, resolvedOptions) : resolveMatchedLocalLink(resolvedNameLink, resolvedOptions);
|
|
1821
1823
|
}
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
continue;
|
|
1827
|
-
}
|
|
1828
|
-
console.warn(`Failed to convert file URL to path: ${currentPathOrUrl} --> ${resolvedUrl}`);
|
|
1829
|
-
return resolvedUrl;
|
|
1830
|
-
}
|
|
1831
|
-
switch (sourceType) {
|
|
1832
|
-
case "localFilePath": return resolveLocalFilePath(decodedUrl, {
|
|
1833
|
-
allFilePaths,
|
|
1834
|
-
basePath,
|
|
1835
|
-
convertFilePathsToProtocol,
|
|
1836
|
-
cwd,
|
|
1837
|
-
obsidianVaultName,
|
|
1838
|
-
type
|
|
1839
|
-
});
|
|
1840
|
-
case "obsidianVaultUrl": return currentPathOrUrl;
|
|
1841
|
-
case "remoteHttpUrl": return currentPathOrUrl;
|
|
1842
|
-
case "unsupportedProtocolUrl":
|
|
1843
|
-
console.warn(`Unsupported URL protocol: ${currentPathOrUrl}`);
|
|
1844
|
-
return currentPathOrUrl;
|
|
1824
|
+
case "localFilePath": return resolveLocalFilePath(decodedUrl, resolvedOptions);
|
|
1825
|
+
case "localFileUrl": {
|
|
1826
|
+
const encodedFilePath = fileUrlToPath(filePathOrUrl);
|
|
1827
|
+
return resolveLocalFilePath(safeDecodeURI(encodedFilePath) ?? encodedFilePath, resolvedOptions);
|
|
1845
1828
|
}
|
|
1829
|
+
case "obsidianVaultUrl":
|
|
1830
|
+
case "remoteHttpUrl": return filePathOrUrl;
|
|
1831
|
+
case "unsupportedProtocolUrl":
|
|
1832
|
+
console.warn(`Unsupported URL protocol: ${filePathOrUrl}`);
|
|
1833
|
+
return filePathOrUrl;
|
|
1846
1834
|
}
|
|
1847
1835
|
}
|
|
1848
1836
|
/**
|
|
@@ -1851,20 +1839,33 @@ function resolveLink(filePathOrUrl, options) {
|
|
|
1851
1839
|
* `max-depth` lint rule).
|
|
1852
1840
|
*/
|
|
1853
1841
|
function resolveLocalFilePath(decodedUrl, options) {
|
|
1854
|
-
const { allFilePaths, basePath,
|
|
1855
|
-
const
|
|
1842
|
+
const { allFilePaths, basePath, cwd } = options;
|
|
1843
|
+
const candidates = getLocalPathCandidates(decodedUrl);
|
|
1844
|
+
for (const { anchor, filePath } of candidates) {
|
|
1845
|
+
const resolvedBase = resolveWithBasePath(normalize(filePath), {
|
|
1846
|
+
basePath,
|
|
1847
|
+
cwd
|
|
1848
|
+
});
|
|
1849
|
+
const matchedPath = (path.extname(resolvedBase) === "" ? [`${resolvedBase}.md`] : [resolvedBase, `${resolvedBase}.md`]).find((candidate) => pathExistsInAllFiles(candidate, allFilePaths ?? []));
|
|
1850
|
+
if (matchedPath !== void 0) return resolveMatchedLocalLink({
|
|
1851
|
+
anchor,
|
|
1852
|
+
filePath: matchedPath
|
|
1853
|
+
}, options);
|
|
1854
|
+
}
|
|
1855
|
+
return resolveWithBasePath(normalize(candidates.at(-1)?.filePath ?? decodedUrl), {
|
|
1856
1856
|
basePath,
|
|
1857
1857
|
cwd
|
|
1858
1858
|
});
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
return
|
|
1859
|
+
}
|
|
1860
|
+
/** Render a matched local file and its still-unmodified Obsidian anchor. */
|
|
1861
|
+
function resolveMatchedLocalLink(link, options) {
|
|
1862
|
+
const { anchor, filePath } = link;
|
|
1863
|
+
const { basePath, convertFilePathsToProtocol, obsidianVaultName, type } = options;
|
|
1864
|
+
if (convertFilePathsToProtocol !== "none" && (type === "link" || type === "embed" && [".md", ".pdf"].includes(path.extname(filePath)))) {
|
|
1865
|
+
if (convertFilePathsToProtocol === "obsidian" && obsidianVaultName !== void 0) return createObsidianVaultLink(link, basePath ?? "", obsidianVaultName);
|
|
1866
|
+
if (convertFilePathsToProtocol === "file") return createFileLink(`${filePath}${anchor ?? ""}`);
|
|
1866
1867
|
}
|
|
1867
|
-
return
|
|
1868
|
+
return filePath;
|
|
1868
1869
|
}
|
|
1869
1870
|
/**
|
|
1870
1871
|
* Convert from a (usually wiki-style) named link to an absolute path to an
|
|
@@ -1875,27 +1876,52 @@ function resolveLocalFilePath(decodedUrl, options) {
|
|
|
1875
1876
|
* Obsidian seems to treat note links slightly differently from image / asset
|
|
1876
1877
|
* links.
|
|
1877
1878
|
*
|
|
1878
|
-
* @param name Non-URI-encoded name of the file,
|
|
1879
|
-
*
|
|
1880
|
-
*
|
|
1879
|
+
* @param name Non-URI-encoded, normalized name of the file, with or without a
|
|
1880
|
+
* file extension. Extension-less names are assumed to be .md files. May
|
|
1881
|
+
* include an anchor suffix, or anchor delimiter characters that are a literal
|
|
1882
|
+
* part of the file name. (POSIX-style paths.)
|
|
1881
1883
|
* @param cwd Absolute path to the current working directory of the file from
|
|
1882
1884
|
* which we're resolving the link. (POSIX-style paths)
|
|
1883
1885
|
* @param allFilePaths Array of absolute paths to all other files in the paths
|
|
1884
1886
|
* to be considered. (POSIX-style paths.)
|
|
1885
1887
|
*
|
|
1886
|
-
* @returns
|
|
1887
|
-
* undefined if there's no valid match. (POSIX-style
|
|
1888
|
+
* @returns The absolute path to the best matching file and its unmodified
|
|
1889
|
+
* anchor, or undefined if there's no valid match. (POSIX-style path.)
|
|
1888
1890
|
*/
|
|
1889
1891
|
function resolveNameLink(name, cwd, allFilePaths) {
|
|
1890
1892
|
if (allFilePaths.length === 0) return;
|
|
1891
|
-
const
|
|
1893
|
+
for (const { anchor, filePath } of getLocalPathCandidates(name)) {
|
|
1894
|
+
const normalizedBase = normalize(filePath);
|
|
1895
|
+
const match = findBestNameMatch(path.extname(normalizedBase) === "" ? `${normalizedBase}.md` : normalizedBase, cwd, allFilePaths);
|
|
1896
|
+
if (match !== void 0) return {
|
|
1897
|
+
anchor,
|
|
1898
|
+
filePath: match
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
/**
|
|
1903
|
+
* Find the best matching file for a name in the list of all file paths.
|
|
1904
|
+
* Extracted from `resolveNameLink` so each base and anchor interpretation of
|
|
1905
|
+
* the name can be attempted in turn.
|
|
1906
|
+
*
|
|
1907
|
+
* @param base Non-URI-encoded name of the file with a file extension and no
|
|
1908
|
+
* anchor suffix. (POSIX-style paths.)
|
|
1909
|
+
* @param cwd Absolute path to the current working directory of the file from
|
|
1910
|
+
* which we're resolving the link. (POSIX-style paths)
|
|
1911
|
+
* @param allFilePaths Array of absolute paths to all other files in the paths
|
|
1912
|
+
* to be considered. (POSIX-style paths.)
|
|
1913
|
+
*
|
|
1914
|
+
* @returns Absolute path to the best matching file, or undefined if there's no
|
|
1915
|
+
* valid match. (POSIX-style paths.)
|
|
1916
|
+
*/
|
|
1917
|
+
function findBestNameMatch(base, cwd, allFilePaths) {
|
|
1892
1918
|
const baseWithoutMd = base.replace(MD_EXTENSION_REGEX, "").toLowerCase();
|
|
1893
1919
|
const pathsToName = allFilePaths.filter((filePath) => {
|
|
1894
1920
|
return filePath.replace(MD_EXTENSION_REGEX, "").toLowerCase().endsWith(baseWithoutMd);
|
|
1895
1921
|
});
|
|
1896
1922
|
if (pathsToName.length === 0) return;
|
|
1897
|
-
if (pathsToName.length === 1) return
|
|
1898
|
-
return
|
|
1923
|
+
if (pathsToName.length === 1) return pathsToName[0];
|
|
1924
|
+
return pathsToName.toSorted((a, b) => {
|
|
1899
1925
|
if (!base.endsWith(".md") || base.includes(path.sep)) {
|
|
1900
1926
|
const aHasCwd = a.startsWith(cwd);
|
|
1901
1927
|
if (aHasCwd !== b.startsWith(cwd)) return aHasCwd ? -1 : 1;
|
|
@@ -1904,29 +1930,29 @@ function resolveNameLink(name, cwd, allFilePaths) {
|
|
|
1904
1930
|
const bDepth = b.split(path.sep).length;
|
|
1905
1931
|
if (aDepth !== bDepth) return aDepth - bDepth;
|
|
1906
1932
|
return a.localeCompare(b);
|
|
1907
|
-
})[0]
|
|
1933
|
+
})[0];
|
|
1908
1934
|
}
|
|
1909
1935
|
/**
|
|
1910
|
-
* Check for presence of a path in a list in a case-
|
|
1911
|
-
*
|
|
1936
|
+
* Check for presence of a path in a list in a case-agnostic manner, since
|
|
1937
|
+
* Obsidian is not case sensitive.
|
|
1912
1938
|
*
|
|
1913
|
-
* @param filePath
|
|
1939
|
+
* @param filePath Literal file path with file extension and no anchor suffix.
|
|
1940
|
+
* (POSIX-style path.)
|
|
1914
1941
|
* @param allFilePaths Array of absolute file paths to check. (POSIX-style
|
|
1915
1942
|
* paths.)
|
|
1916
1943
|
*
|
|
1917
|
-
* @returns
|
|
1918
|
-
* undefined if it is not.
|
|
1944
|
+
* @returns Whether the file path is present in the list of all file paths.
|
|
1919
1945
|
*/
|
|
1920
1946
|
function pathExistsInAllFiles(filePath, allFilePaths) {
|
|
1921
|
-
const
|
|
1922
|
-
|
|
1947
|
+
const filePathLowerCase = filePath.toLowerCase();
|
|
1948
|
+
return allFilePaths.some((file) => file.toLowerCase().endsWith(filePathLowerCase));
|
|
1923
1949
|
}
|
|
1924
1950
|
function createFileLink(absolutePath) {
|
|
1925
1951
|
return `file://${absolutePath}`;
|
|
1926
1952
|
}
|
|
1927
|
-
function createObsidianVaultLink(
|
|
1928
|
-
const
|
|
1929
|
-
return `obsidian://open?vault=${encodeURIComponent(obsidianVault)}&file=${encodeURIComponent(
|
|
1953
|
+
function createObsidianVaultLink(link, basePath, obsidianVault) {
|
|
1954
|
+
const linkPath = `${stripBasePath(link.filePath, basePath)}${link.anchor ?? ""}`;
|
|
1955
|
+
return `obsidian://open?vault=${encodeURIComponent(obsidianVault)}&file=${encodeURIComponent(linkPath)}`;
|
|
1930
1956
|
}
|
|
1931
1957
|
//#endregion
|
|
1932
1958
|
//#region src/lib/parse/remark-resolve-links.ts
|
|
@@ -1948,7 +1974,7 @@ const plugin$1 = function(options) {
|
|
|
1948
1974
|
obsidianVaultName: obsidianVault,
|
|
1949
1975
|
type: "link"
|
|
1950
1976
|
});
|
|
1951
|
-
node.url = isUrl(resolvedLink) ? resolvedLink : encodeURI(resolvedLink);
|
|
1977
|
+
node.url = isUrl(resolvedLink) ? resolvedLink : encodeURI(resolvedLink).replaceAll("?", "%3F").replaceAll("#", "%23");
|
|
1952
1978
|
});
|
|
1953
1979
|
visit(tree, "image", (node) => {
|
|
1954
1980
|
node.data ??= {};
|