yanki 2.0.11 → 2.0.12

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/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("[#?^]", "v");
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,37 @@ 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 file path whose name
580
+ * may contain anchor delimiter characters (`#`, `^`), ordered from the longest
581
+ * literal file name (no anchor at all) to the shortest (anchor starts at the
582
+ * first delimiter in the file name, matching Obsidian's anchor syntax).
583
+ * Delimiters in directory names are never treated as anchor starts.
584
+ *
585
+ * File names may legitimately contain these characters, so callers should try
586
+ * candidates in order against a list of real files instead of assuming the
587
+ * first delimiter starts an anchor.
588
+ * https://github.com/kitschpatrol/yanki/issues/20
589
+ */
590
+ function getBaseAndQueryCandidates(filePath) {
591
+ const directoryPath = path.dirname(filePath);
592
+ const fileName = path.basename(filePath);
593
+ const candidates = [{
594
+ base: path.join(directoryPath, fileName),
595
+ query: void 0
596
+ }];
597
+ for (let index = fileName.length - 1; index >= 0; index--) if (QUERY_FRAGMENT_START_REGEX.test(fileName.charAt(index))) candidates.push({
598
+ base: path.join(directoryPath, fileName.slice(0, index)),
599
+ query: fileName.slice(index)
600
+ });
601
+ return candidates;
602
+ }
578
603
  function getBase(filePath) {
579
604
  return getBaseAndQueryParts(filePath)[0];
580
605
  }
581
606
  function getQuery(filePath) {
582
607
  return getBaseAndQueryParts(filePath).at(1) ?? "";
583
608
  }
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
609
  //#endregion
599
610
  //#region src/lib/utilities/url.ts
600
611
  const DRIVE_LETTER_REGEX = /* @__PURE__ */ new RegExp("^[a-z]:", "iv");
@@ -1792,7 +1803,9 @@ const defaultResolveLinkOptions = {
1792
1803
  * @returns Resolved absolute path or URL One of:
1793
1804
  *
1794
1805
  * - Resolved absolute POSIX-style paths
1795
- * - Removes any file path query parameters
1806
+ * - Removes any Obsidian-style heading and block anchors, unless the anchor
1807
+ * characters are a literal part of a matched file's name (`?` is always
1808
+ * treated as a literal file name character in local paths)
1796
1809
  * - Not URI-encoded
1797
1810
  * - Retains original case
1798
1811
  * - HTTP protocol URL
@@ -1807,8 +1820,7 @@ function resolveLink(filePathOrUrl, options) {
1807
1820
  const decodedUrl = safeDecodeURI(currentPathOrUrl) ?? currentPathOrUrl;
1808
1821
  const sourceType = getSrcType(decodedUrl);
1809
1822
  if (sourceType === "localFileName") {
1810
- let resolvedUrl = addExtensionIfMissing(normalize(decodedUrl), "md");
1811
- resolvedUrl = resolveNameLink(resolvedUrl, cwd, allFilePaths ?? []) ?? resolveWithBasePath(decodedUrl, {
1823
+ const resolvedUrl = resolveNameLink(normalize(decodedUrl), cwd, allFilePaths ?? []) ?? resolveWithBasePath(decodedUrl, {
1812
1824
  basePath,
1813
1825
  cwd
1814
1826
  });
@@ -1856,13 +1868,21 @@ function resolveLocalFilePath(decodedUrl, options) {
1856
1868
  basePath,
1857
1869
  cwd
1858
1870
  });
1859
- const resolvedUrlWithMatchedExtension = pathExistsInAllFiles(addExtensionIfMissing(resolvedUrl, "md"), allFilePaths ?? []) ?? pathExistsInAllFiles(addExtension(resolvedUrl, "md"), allFilePaths ?? []) ?? void 0;
1860
- if (resolvedUrlWithMatchedExtension !== void 0) {
1861
- if (convertFilePathsToProtocol !== "none" && (type === "link" || type === "embed" && [".md", ".pdf"].includes(getExtension(resolvedUrlWithMatchedExtension)))) {
1862
- if (convertFilePathsToProtocol === "obsidian" && obsidianVaultName !== void 0) return createObsidianVaultLink(resolvedUrlWithMatchedExtension, basePath ?? "", obsidianVaultName);
1863
- if (convertFilePathsToProtocol === "file") return createFileLink(resolvedUrlWithMatchedExtension);
1871
+ let matchedPath;
1872
+ let matchedQuery;
1873
+ for (const { base, query } of getBaseAndQueryCandidates(resolvedUrl)) {
1874
+ matchedPath = (path.extname(base) === "" ? [`${base}.md`] : [base, `${base}.md`]).find((candidate) => pathExistsInAllFiles(candidate, allFilePaths ?? []));
1875
+ if (matchedPath !== void 0) {
1876
+ matchedQuery = query;
1877
+ break;
1878
+ }
1879
+ }
1880
+ if (matchedPath !== void 0) {
1881
+ if (convertFilePathsToProtocol !== "none" && (type === "link" || type === "embed" && [".md", ".pdf"].includes(path.extname(matchedPath)))) {
1882
+ if (convertFilePathsToProtocol === "obsidian" && obsidianVaultName !== void 0) return createObsidianVaultLink(`${matchedPath}${matchedQuery ?? ""}`, basePath ?? "", obsidianVaultName);
1883
+ if (convertFilePathsToProtocol === "file") return createFileLink(`${matchedPath}${matchedQuery ?? ""}`);
1864
1884
  }
1865
- return getBase(resolvedUrlWithMatchedExtension);
1885
+ return matchedPath;
1866
1886
  }
1867
1887
  return getBase(resolvedUrl);
1868
1888
  }
@@ -1875,9 +1895,10 @@ function resolveLocalFilePath(decodedUrl, options) {
1875
1895
  * Obsidian seems to treat note links slightly differently from image / asset
1876
1896
  * links.
1877
1897
  *
1878
- * @param name Non-URI-encoded name of the file, may have file extension, if no
1879
- * match with a non-.md extension is found, a match will be attempted with .md
1880
- * regardless. (POSIX-style paths.)
1898
+ * @param name Non-URI-encoded, normalized name of the file, with or without a
1899
+ * file extension. Extension-less names are assumed to be .md files. May
1900
+ * include an anchor suffix, or anchor delimiter characters that are a literal
1901
+ * part of the file name. (POSIX-style paths.)
1881
1902
  * @param cwd Absolute path to the current working directory of the file from
1882
1903
  * which we're resolving the link. (POSIX-style paths)
1883
1904
  * @param allFilePaths Array of absolute paths to all other files in the paths
@@ -1888,14 +1909,34 @@ function resolveLocalFilePath(decodedUrl, options) {
1888
1909
  */
1889
1910
  function resolveNameLink(name, cwd, allFilePaths) {
1890
1911
  if (allFilePaths.length === 0) return;
1891
- const [base, query] = getBaseAndQueryParts(name);
1912
+ for (const { base, query } of getBaseAndQueryCandidates(name)) {
1913
+ const match = findBestNameMatch(path.extname(base) === "" ? `${base}.md` : base, cwd, allFilePaths);
1914
+ if (match !== void 0) return `${match}${query ?? ""}`;
1915
+ }
1916
+ }
1917
+ /**
1918
+ * Find the best matching file for a name in the list of all file paths.
1919
+ * Extracted from `resolveNameLink` so each base and anchor interpretation of
1920
+ * the name can be attempted in turn.
1921
+ *
1922
+ * @param base Non-URI-encoded name of the file with a file extension and no
1923
+ * anchor suffix. (POSIX-style paths.)
1924
+ * @param cwd Absolute path to the current working directory of the file from
1925
+ * which we're resolving the link. (POSIX-style paths)
1926
+ * @param allFilePaths Array of absolute paths to all other files in the paths
1927
+ * to be considered. (POSIX-style paths.)
1928
+ *
1929
+ * @returns Absolute path to the best matching file, or undefined if there's no
1930
+ * valid match. (POSIX-style paths.)
1931
+ */
1932
+ function findBestNameMatch(base, cwd, allFilePaths) {
1892
1933
  const baseWithoutMd = base.replace(MD_EXTENSION_REGEX, "").toLowerCase();
1893
1934
  const pathsToName = allFilePaths.filter((filePath) => {
1894
1935
  return filePath.replace(MD_EXTENSION_REGEX, "").toLowerCase().endsWith(baseWithoutMd);
1895
1936
  });
1896
1937
  if (pathsToName.length === 0) return;
1897
- if (pathsToName.length === 1) return `${pathsToName[0]}${query ?? ""}`;
1898
- return `${pathsToName.toSorted((a, b) => {
1938
+ if (pathsToName.length === 1) return pathsToName[0];
1939
+ return pathsToName.toSorted((a, b) => {
1899
1940
  if (!base.endsWith(".md") || base.includes(path.sep)) {
1900
1941
  const aHasCwd = a.startsWith(cwd);
1901
1942
  if (aHasCwd !== b.startsWith(cwd)) return aHasCwd ? -1 : 1;
@@ -1904,22 +1945,22 @@ function resolveNameLink(name, cwd, allFilePaths) {
1904
1945
  const bDepth = b.split(path.sep).length;
1905
1946
  if (aDepth !== bDepth) return aDepth - bDepth;
1906
1947
  return a.localeCompare(b);
1907
- })[0]}${query ?? ""}`;
1948
+ })[0];
1908
1949
  }
1909
1950
  /**
1910
- * Check for presence of a path in a list in a case- and query- agnostic manner.
1911
- * Ignores .md extensions to simplify matching files
1951
+ * Check for presence of a path in a list in a case-agnostic manner, since
1952
+ * Obsidian is not case sensitive.
1912
1953
  *
1913
- * @param filePath File path with file extension. (POSIX-style path.)
1954
+ * @param filePath Literal file path with file extension and no anchor suffix.
1955
+ * (POSIX-style path.)
1914
1956
  * @param allFilePaths Array of absolute file paths to check. (POSIX-style
1915
1957
  * paths.)
1916
1958
  *
1917
- * @returns The file path if it is present in the list of all file paths, or
1918
- * undefined if it is not.
1959
+ * @returns Whether the file path is present in the list of all file paths.
1919
1960
  */
1920
1961
  function pathExistsInAllFiles(filePath, allFilePaths) {
1921
- const base = getBase(filePath);
1922
- if (allFilePaths.some((file) => file.toLowerCase().endsWith(base.toLowerCase()))) return filePath;
1962
+ const filePathLowerCase = filePath.toLowerCase();
1963
+ return allFilePaths.some((file) => file.toLowerCase().endsWith(filePathLowerCase));
1923
1964
  }
1924
1965
  function createFileLink(absolutePath) {
1925
1966
  return `file://${absolutePath}`;
@@ -1948,7 +1989,7 @@ const plugin$1 = function(options) {
1948
1989
  obsidianVaultName: obsidianVault,
1949
1990
  type: "link"
1950
1991
  });
1951
- node.url = isUrl(resolvedLink) ? resolvedLink : encodeURI(resolvedLink);
1992
+ node.url = isUrl(resolvedLink) ? resolvedLink : encodeURI(resolvedLink).replaceAll("?", "%3F").replaceAll("#", "%23");
1952
1993
  });
1953
1994
  visit(tree, "image", (node) => {
1954
1995
  node.data ??= {};