yanki 2.0.10 → 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.
@@ -1,6 +1,5 @@
1
1
  import { YankiConnectOptions, YankiFetchAdapter, YankiParamsForAction } from "yanki-connect";
2
2
  import { PartialDeep, Simplify } from "type-fest";
3
-
4
3
  //#region src/lib/model/model.d.ts
5
4
  declare const yankiModels: [{
6
5
  readonly cardTemplates: [{
@@ -109,8 +108,10 @@ type GlobalOptions = {
109
108
  * user-specified file names in other cases.
110
109
  */
111
110
  maxFilenameLength: number;
112
- namespace: string; /** Ensures that wiki-style links work correctly */
113
- obsidianVault: string | undefined; /** Exposed for testing only */
111
+ namespace: string;
112
+ /** Ensures that wiki-style links work correctly */
113
+ obsidianVault: string | undefined;
114
+ /** Exposed for testing only */
114
115
  resolveUrls: boolean;
115
116
  /**
116
117
  * Whether to treat single newlines in Markdown as line breaks in the
@@ -122,7 +123,8 @@ type GlobalOptions = {
122
123
  * be equivalent, don't match local notes with "orphaned" remote notes based
123
124
  * on content
124
125
  */
125
- strictMatching: boolean; /** Sync image, video, and audio assets to Anki's media storage system */
126
+ strictMatching: boolean;
127
+ /** Sync image, video, and audio assets to Anki's media storage system */
126
128
  syncMediaAssets: SyncMediaAssets;
127
129
  };
128
130
  //#endregion
@@ -171,12 +173,12 @@ type LocalNote = {
171
173
  };
172
174
  //#endregion
173
175
  //#region src/lib/actions/rename.d.ts
174
- type RenameNotesOptions = Pick<GlobalOptions, 'dryRun' | 'fileAdapter' | 'manageFilenames' | 'maxFilenameLength'
176
+ type RenameNotesOptions = Pick<GlobalOptions, 'dryRun' | 'fileAdapter' | 'manageFilenames' | 'maxFilenameLength' |
175
177
  /**
176
178
  * Included because this can technically change the content of the "first
177
179
  * line" of a card
178
180
  */
179
- | 'strictLineBreaks'>;
181
+ 'strictLineBreaks'>;
180
182
  type RenameFilesResult = {
181
183
  dryRun: boolean;
182
184
  notes: LocalNote[];
package/dist/lib/index.js CHANGED
@@ -110,7 +110,7 @@ const NEWLINE_REGEX$1 = /* @__PURE__ */ new RegExp("\\r?\\n", "v");
110
110
  const LEADING_WHITESPACE_REGEX = /* @__PURE__ */ new RegExp("^(\\s+)", "v");
111
111
  function trimLeadingIndentation(strings, ...values) {
112
112
  const lines = strings.reduce((result, text, i) => `${result}${text}${String(values[i] ?? "")}`, "").split(NEWLINE_REGEX$1).filter((line) => line.trim() !== "");
113
- const leadingSpace = LEADING_WHITESPACE_REGEX.exec(lines[0])?.[0] ?? "";
113
+ const leadingSpace = LEADING_WHITESPACE_REGEX.exec(lines[0] ?? "")?.[0] ?? "";
114
114
  const leadingSpaceRegex = new RegExp(`^${leadingSpace}`, "v");
115
115
  return lines.map((line) => line.replace(leadingSpaceRegex, "").trimEnd()).join("\n");
116
116
  }
@@ -502,14 +502,14 @@ function getFileExtensionForMimeType(mimeType) {
502
502
  "video/x-matroska": "mkv",
503
503
  "video/x-msvideo": "avi"
504
504
  };
505
- const normalizedMimeType = mimeType.split(";", 1)[0].trim().toLowerCase();
505
+ const normalizedMimeType = (mimeType.split(";", 1)[0] ?? "").trim().toLowerCase();
506
506
  if (normalizedMimeType === "") return;
507
507
  return mimeToExtension[normalizedMimeType];
508
508
  }
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");
@@ -718,11 +729,19 @@ async function getUrlContentHash(url, fetchAdapter, mode = MEDIA_DEFAULT_HASH_MO
718
729
  } catch {}
719
730
  return getHash(url, 16);
720
731
  }
732
+ const DEFAULT_PORT_BY_PROTOCOL = {
733
+ "ftp:": 21,
734
+ "http:": 80,
735
+ "https:": 443,
736
+ "ws:": 80,
737
+ "wss:": 443
738
+ };
721
739
  function urlToHostAndPort(url) {
722
740
  const parsedUrl = safeParseUrl(url);
723
- return parsedUrl === void 0 ? void 0 : {
741
+ if (parsedUrl === void 0) return;
742
+ return {
724
743
  host: `${parsedUrl.protocol}//${parsedUrl.hostname}`,
725
- port: Number.parseInt(parsedUrl.port, 10)
744
+ port: parsedUrl.port === "" ? DEFAULT_PORT_BY_PROTOCOL[parsedUrl.protocol] ?? NaN : Number(parsedUrl.port)
726
745
  };
727
746
  }
728
747
  function hostAndPortToUrl(host, port) {
@@ -786,7 +805,7 @@ const plugin$3 = function() {
786
805
  visit(tree, (node, index, parent) => {
787
806
  if (parent === void 0 || index === void 0 || node.type !== "element") return CONTINUE;
788
807
  const [preChild] = node.children;
789
- if (node.tagName === "pre" && node.children.length === 1 && preChild.type === "element" && preChild.tagName === "code" && Array.isArray(preChild.properties.className) && preChild.properties.className.includes("language-math")) {
808
+ if (node.tagName === "pre" && node.children.length === 1 && preChild?.type === "element" && preChild.tagName === "code" && Array.isArray(preChild.properties.className) && preChild.properties.className.includes("language-math")) {
790
809
  fenced = true;
791
810
  parent.children[index] = preChild;
792
811
  }
@@ -807,6 +826,7 @@ const plugin$3 = function() {
807
826
  }
808
827
  ];
809
828
  }
829
+ return CONTINUE;
810
830
  });
811
831
  };
812
832
  };
@@ -943,15 +963,17 @@ async function mdastToHtml(mdast, options) {
943
963
  tagName: "span"
944
964
  }, [u("text", `[sound:${finalSrc}]`)]);
945
965
  });
966
+ return CONTINUE;
946
967
  });
947
968
  for (const mutationPromise of treeMutationPromises) await mutationPromise();
948
969
  visit(hastWithClass, "element", (node, index, parent) => {
949
970
  if (parent === void 0 || index === void 0 || node.tagName !== "img" || emptyIsUndefined(String(node.properties.alt)) === void 0) return CONTINUE;
950
- const { alt, height, width } = parseDimensionsFromAltText(String(node.properties.alt ?? ""));
971
+ const { alt, height, width } = parseDimensionsFromAltText(node.properties.alt ?? "");
951
972
  if (alt === void 0) delete node.properties.alt;
952
973
  else node.properties.alt = alt;
953
974
  if (height !== void 0) node.properties.height = height;
954
975
  if (width !== void 0) node.properties.width = width;
976
+ return CONTINUE;
955
977
  });
956
978
  const isEmpty = isVisuallyEmpty(hastWithClass);
957
979
  if (isEmpty && !useEmptyPlaceholder) return "";
@@ -1034,7 +1056,7 @@ function parseDimensions(dimensions) {
1034
1056
  function isVisuallyEmpty(tree) {
1035
1057
  let hasVisualContent = false;
1036
1058
  visit(tree, (node) => {
1037
- if (hasVisualContent) return;
1059
+ if (hasVisualContent) return EXIT;
1038
1060
  if (node.type === "element") {
1039
1061
  const element = node;
1040
1062
  const { tagName } = element;
@@ -1061,6 +1083,7 @@ function isVisuallyEmpty(tree) {
1061
1083
  hasVisualContent = true;
1062
1084
  return EXIT;
1063
1085
  }
1086
+ return CONTINUE;
1064
1087
  });
1065
1088
  return !hasVisualContent;
1066
1089
  }
@@ -1304,6 +1327,9 @@ function areTagsEqual(localTags, remoteTags) {
1304
1327
  async function getRemoteNotes(client, namespace = defaultGlobalOptions.namespace) {
1305
1328
  return (await getRemoteNotesById(client, await client.note.findNotes({ query: `"YankiNamespace:${namespace}"` }))).filter((note) => note !== void 0);
1306
1329
  }
1330
+ function getFieldValue(field) {
1331
+ return field?.value ?? "";
1332
+ }
1307
1333
  /**
1308
1334
  * Get all data from Anki required to populate the YankiNote type.
1309
1335
  *
@@ -1355,10 +1381,10 @@ async function getRemoteNotesById(client, noteIds) {
1355
1381
  cards: ankiNote.cards,
1356
1382
  deckName,
1357
1383
  fields: {
1358
- Back: ankiNote.fields.Back.value ?? "",
1359
- ...ankiNote.fields.Extra !== void 0 && { Extra: ankiNote.fields.Extra.value ?? "" },
1360
- Front: ankiNote.fields.Front.value ?? "",
1361
- YankiNamespace: ankiNote.fields.YankiNamespace.value ?? ""
1384
+ Back: getFieldValue(ankiNote.fields.Back),
1385
+ ...ankiNote.fields.Extra !== void 0 && { Extra: getFieldValue(ankiNote.fields.Extra) },
1386
+ Front: getFieldValue(ankiNote.fields.Front),
1387
+ YankiNamespace: getFieldValue(ankiNote.fields.YankiNamespace)
1362
1388
  },
1363
1389
  modelName: ankiNote.modelName,
1364
1390
  noteId: ankiNote.noteId,
@@ -1777,7 +1803,9 @@ const defaultResolveLinkOptions = {
1777
1803
  * @returns Resolved absolute path or URL One of:
1778
1804
  *
1779
1805
  * - Resolved absolute POSIX-style paths
1780
- * - 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)
1781
1809
  * - Not URI-encoded
1782
1810
  * - Retains original case
1783
1811
  * - HTTP protocol URL
@@ -1792,8 +1820,7 @@ function resolveLink(filePathOrUrl, options) {
1792
1820
  const decodedUrl = safeDecodeURI(currentPathOrUrl) ?? currentPathOrUrl;
1793
1821
  const sourceType = getSrcType(decodedUrl);
1794
1822
  if (sourceType === "localFileName") {
1795
- let resolvedUrl = addExtensionIfMissing(normalize(decodedUrl), "md");
1796
- resolvedUrl = resolveNameLink(resolvedUrl, cwd, allFilePaths ?? []) ?? resolveWithBasePath(decodedUrl, {
1823
+ const resolvedUrl = resolveNameLink(normalize(decodedUrl), cwd, allFilePaths ?? []) ?? resolveWithBasePath(decodedUrl, {
1797
1824
  basePath,
1798
1825
  cwd
1799
1826
  });
@@ -1841,13 +1868,21 @@ function resolveLocalFilePath(decodedUrl, options) {
1841
1868
  basePath,
1842
1869
  cwd
1843
1870
  });
1844
- const resolvedUrlWithMatchedExtension = pathExistsInAllFiles(addExtensionIfMissing(resolvedUrl, "md"), allFilePaths ?? []) ?? pathExistsInAllFiles(addExtension(resolvedUrl, "md"), allFilePaths ?? []) ?? void 0;
1845
- if (resolvedUrlWithMatchedExtension !== void 0) {
1846
- if (convertFilePathsToProtocol !== "none" && (type === "link" || type === "embed" && [".md", ".pdf"].includes(getExtension(resolvedUrlWithMatchedExtension)))) {
1847
- if (convertFilePathsToProtocol === "obsidian" && obsidianVaultName !== void 0) return createObsidianVaultLink(resolvedUrlWithMatchedExtension, basePath ?? "", obsidianVaultName);
1848
- 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 ?? ""}`);
1849
1884
  }
1850
- return getBase(resolvedUrlWithMatchedExtension);
1885
+ return matchedPath;
1851
1886
  }
1852
1887
  return getBase(resolvedUrl);
1853
1888
  }
@@ -1860,9 +1895,10 @@ function resolveLocalFilePath(decodedUrl, options) {
1860
1895
  * Obsidian seems to treat note links slightly differently from image / asset
1861
1896
  * links.
1862
1897
  *
1863
- * @param name Non-URI-encoded name of the file, may have file extension, if no
1864
- * match with a non-.md extension is found, a match will be attempted with .md
1865
- * 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.)
1866
1902
  * @param cwd Absolute path to the current working directory of the file from
1867
1903
  * which we're resolving the link. (POSIX-style paths)
1868
1904
  * @param allFilePaths Array of absolute paths to all other files in the paths
@@ -1873,14 +1909,34 @@ function resolveLocalFilePath(decodedUrl, options) {
1873
1909
  */
1874
1910
  function resolveNameLink(name, cwd, allFilePaths) {
1875
1911
  if (allFilePaths.length === 0) return;
1876
- 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) {
1877
1933
  const baseWithoutMd = base.replace(MD_EXTENSION_REGEX, "").toLowerCase();
1878
1934
  const pathsToName = allFilePaths.filter((filePath) => {
1879
1935
  return filePath.replace(MD_EXTENSION_REGEX, "").toLowerCase().endsWith(baseWithoutMd);
1880
1936
  });
1881
1937
  if (pathsToName.length === 0) return;
1882
- if (pathsToName.length === 1) return `${pathsToName[0]}${query ?? ""}`;
1883
- return `${pathsToName.toSorted((a, b) => {
1938
+ if (pathsToName.length === 1) return pathsToName[0];
1939
+ return pathsToName.toSorted((a, b) => {
1884
1940
  if (!base.endsWith(".md") || base.includes(path.sep)) {
1885
1941
  const aHasCwd = a.startsWith(cwd);
1886
1942
  if (aHasCwd !== b.startsWith(cwd)) return aHasCwd ? -1 : 1;
@@ -1889,22 +1945,22 @@ function resolveNameLink(name, cwd, allFilePaths) {
1889
1945
  const bDepth = b.split(path.sep).length;
1890
1946
  if (aDepth !== bDepth) return aDepth - bDepth;
1891
1947
  return a.localeCompare(b);
1892
- })[0]}${query ?? ""}`;
1948
+ })[0];
1893
1949
  }
1894
1950
  /**
1895
- * Check for presence of a path in a list in a case- and query- agnostic manner.
1896
- * 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.
1897
1953
  *
1898
- * @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.)
1899
1956
  * @param allFilePaths Array of absolute file paths to check. (POSIX-style
1900
1957
  * paths.)
1901
1958
  *
1902
- * @returns The file path if it is present in the list of all file paths, or
1903
- * undefined if it is not.
1959
+ * @returns Whether the file path is present in the list of all file paths.
1904
1960
  */
1905
1961
  function pathExistsInAllFiles(filePath, allFilePaths) {
1906
- const base = getBase(filePath);
1907
- 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));
1908
1964
  }
1909
1965
  function createFileLink(absolutePath) {
1910
1966
  return `file://${absolutePath}`;
@@ -1933,7 +1989,7 @@ const plugin$1 = function(options) {
1933
1989
  obsidianVaultName: obsidianVault,
1934
1990
  type: "link"
1935
1991
  });
1936
- node.url = isUrl(resolvedLink) ? resolvedLink : encodeURI(resolvedLink);
1992
+ node.url = isUrl(resolvedLink) ? resolvedLink : encodeURI(resolvedLink).replaceAll("?", "%3F").replaceAll("#", "%23");
1937
1993
  });
1938
1994
  visit(tree, "image", (node) => {
1939
1995
  node.data ??= {};
@@ -2323,7 +2379,7 @@ function replaceDeleteNodesWithClozeMarkup(ast) {
2323
2379
  visit(ast, "delete", (node, index, parent) => {
2324
2380
  if (parent === void 0 || index === void 0 || !("children" in node) || node.children.length === 0) return CONTINUE;
2325
2381
  const [firstChild] = node.children;
2326
- if (node.children.length > 0 && isText(firstChild)) {
2382
+ if (firstChild !== void 0 && isText(firstChild)) {
2327
2383
  const result = CLOZE_NUMBER_PREFIX_REGEX.exec(firstChild.value);
2328
2384
  if (result !== null && (node.children.length > 1 || (result.at(2) ?? "").length > 0)) {
2329
2385
  const possibleClozeIndex = Number(result.at(1));
@@ -2347,6 +2403,7 @@ function replaceDeleteNodesWithClozeMarkup(ast) {
2347
2403
  ];
2348
2404
  parent.children.splice(index, 1, ...clozeNodes);
2349
2405
  clozeIndex += 1;
2406
+ return CONTINUE;
2350
2407
  });
2351
2408
  return ast;
2352
2409
  }
@@ -2379,6 +2436,7 @@ function getYankiModelNameFromTree(ast) {
2379
2436
  probableType = `Yanki - Cloze`;
2380
2437
  return EXIT;
2381
2438
  }
2439
+ return CONTINUE;
2382
2440
  });
2383
2441
  if (probableType !== void 0) return probableType;
2384
2442
  if (!hasThematicBreak(ast) && isLastVisibleNodeEmphasisWithOthers(ast)) return `Yanki - Basic (type in the answer)`;
@@ -2393,6 +2451,7 @@ function getYankiModelNameFromTree(ast) {
2393
2451
  }
2394
2452
  }
2395
2453
  lastNode = node;
2454
+ return CONTINUE;
2396
2455
  });
2397
2456
  return probableType ?? `Yanki - Basic`;
2398
2457
  }
@@ -2428,6 +2487,7 @@ function isLastVisibleNodeEmphasisWithOthers(ast) {
2428
2487
  visibleCount++;
2429
2488
  return SKIP;
2430
2489
  }
2490
+ return CONTINUE;
2431
2491
  });
2432
2492
  return lastVisibleNode?.type === "emphasis" && visibleCount > 1;
2433
2493
  }
@@ -2440,6 +2500,7 @@ function removeLastEmphasis(ast) {
2440
2500
  lastEmphasisNode = node;
2441
2501
  lastEmphasisParent = parent;
2442
2502
  lastEmphasisIndex = index;
2503
+ return CONTINUE;
2443
2504
  });
2444
2505
  if (lastEmphasisParent && lastEmphasisNode && typeof lastEmphasisIndex === "number") {
2445
2506
  lastEmphasisParent.children.splice(lastEmphasisIndex, 1);
@@ -2644,7 +2705,7 @@ async function loadLocalNotes(allLocalFilePaths, options) {
2644
2705
  strictLineBreaks,
2645
2706
  syncMediaAssets
2646
2707
  });
2647
- if (note.deckName === "") note.deckName = deckNamesFromFilePaths[index];
2708
+ if (note.deckName === "") note.deckName = deckNamesFromFilePaths[index] ?? "";
2648
2709
  return {
2649
2710
  filePath,
2650
2711
  filePathOriginal: filePath,
@@ -2771,9 +2832,10 @@ async function getStyle(options) {
2771
2832
  const css = await getModelStyle(client, modelName);
2772
2833
  cssSet.add(css);
2773
2834
  }
2774
- if (cssSet.size === 0) throw new Error("No CSS found in any Yanki model.");
2835
+ const [firstCss] = cssSet;
2836
+ if (firstCss === void 0) throw new Error("No CSS found in any Yanki model.");
2775
2837
  if (cssSet.size > 1) throw new Error("Expected all Yanki models to have identical CSS.");
2776
- return [...cssSet][0];
2838
+ return firstCss;
2777
2839
  }
2778
2840
  async function setStyle(options) {
2779
2841
  const startTime = performance.now();
@@ -2849,7 +2911,7 @@ async function setNoteIdInFrontmatter(markdown, noteId) {
2849
2911
  delete parsedFrontmatter.noteId;
2850
2912
  if (Object.keys(parsedFrontmatter).length === 0) {
2851
2913
  const markdownWithoutFrontmatter = lines.slice(frontmatterEnd + 1);
2852
- if (markdownWithoutFrontmatter[0].trim() === "") return markdownWithoutFrontmatter.slice(1).join("\n");
2914
+ if (markdownWithoutFrontmatter[0]?.trim() === "") return markdownWithoutFrontmatter.slice(1).join("\n");
2853
2915
  return markdownWithoutFrontmatter.join("\n");
2854
2916
  }
2855
2917
  } else parsedFrontmatter.noteId = noteId;
@@ -2862,7 +2924,7 @@ async function setNoteIdInFrontmatter(markdown, noteId) {
2862
2924
  }
2863
2925
  function getFrontmatterRange(markdown) {
2864
2926
  const lines = markdown.split(NEWLINE_REGEX);
2865
- if (!lines.join("").trim().startsWith("---")) return [void 0, void 0];
2927
+ if (!lines.join("").trimStart().startsWith("---")) return [void 0, void 0];
2866
2928
  const frontmatterStart = lines.findIndex((line) => line.startsWith("---"));
2867
2929
  const frontmatterEnd = lines.findIndex((line, index) => index > frontmatterStart && line.startsWith("---"));
2868
2930
  if (frontmatterStart === -1 || frontmatterEnd === -1) return [void 0, void 0];
@@ -3035,7 +3097,7 @@ async function syncFiles(allLocalFilePaths, options) {
3035
3097
  manageFilenames,
3036
3098
  maxFilenameLength
3037
3099
  });
3038
- if (obsidianVault !== void 0) {
3100
+ if (obsidianVault !== void 0 && !dryRun) {
3039
3101
  if (renamedLocalNotes.some((renamedNote) => renamedNote.filePath !== renamedNote.filePathOriginal)) {
3040
3102
  const reloadedLocalNotes = await loadLocalNotes(renamedLocalNotes.map((note) => note.filePath), {
3041
3103
  allFilePaths: allFilePaths.map((filePath) => {
@@ -3099,7 +3161,7 @@ function formatSyncFilesResult(result, verbose = false) {
3099
3161
  }, {});
3100
3162
  const totalSynced = synced.filter((note) => note.action !== "deleted").length;
3101
3163
  const totalRenamed = synced.filter((note) => note.filePath !== note.filePathOriginal).length;
3102
- const ankiUnreachable = actionCounts.ankiUnreachable > 0;
3164
+ const ankiUnreachable = (actionCounts.ankiUnreachable ?? 0) > 0;
3103
3165
  lines.push(`${result.dryRun ? "Will sync" : ankiUnreachable ? "Failed to sync" : "Successfully synced"} ${totalSynced} ${plur("note", totalSynced)} to Anki${result.dryRun ? "" : ` in ${prettyMilliseconds(result.duration)}`}.`);
3104
3166
  if (verbose) {
3105
3167
  lines.push("", result.dryRun ? "Sync Plan Summary:" : "Sync Summary:");