yanki 2.0.4 → 2.0.5

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
@@ -42,10 +42,13 @@ function capitalize(text) {
42
42
  * Truncates on word boundary and adds ellipsis. Does not give special treatment
43
43
  * to file extensions. If there are no spaces in the text, it will truncate at
44
44
  * `maxLength` without respect for word boundaries.
45
+ *
45
46
  * @param text Text to truncate
46
47
  * @param maxLength Maximum length excluding ellipsis
47
48
  * @param truncationString String to append to truncated text. Defaults to '...'
48
- * @param wordBoundary Character to consider a word boundary. Defaults to a space.
49
+ * @param wordBoundary Character to consider a word boundary. Defaults to a
50
+ * space.
51
+ *
49
52
  * @returns Truncated string
50
53
  */
51
54
  function truncateOnWordBoundary(text, maxLength, truncationString = "...", wordBoundary = " ") {
@@ -63,7 +66,9 @@ function emptyIsUndefined(text) {
63
66
  return text.trim() === "" ? void 0 : text;
64
67
  }
65
68
  /**
66
- * Mainly for nice formatting with prettier. But the line wrapping means we have to strip surplus whitespace.
69
+ * Mainly for nice formatting with prettier. But the line wrapping means we have
70
+ * to strip surplus whitespace.
71
+ *
67
72
  * @public
68
73
  */
69
74
  function css(strings, ...values) {
@@ -86,8 +91,10 @@ function splitAtFirstMatch(text, regex) {
86
91
  //#endregion
87
92
  //#region src/lib/shared/constants.ts
88
93
  /**
89
- * The default CSS to use for cards. This matches Anki's default. Stored in the Yanki card models and shared across all Yanki-managed notes regardless of namespace.
90
- * It does change occasionally, see https://github.com/ankitects/anki/blob/main/rslib/src/notetype/styling.css
94
+ * The default CSS to use for cards. This matches Anki's default. Stored in the
95
+ * Yanki card models and shared across all Yanki-managed notes regardless of
96
+ * namespace. It does change occasionally, see
97
+ * https://github.com/ankitects/anki/blob/main/rslib/src/notetype/styling.css
91
98
  */
92
99
  const CSS_DEFAULT_STYLE = css`
93
100
  .card {
@@ -100,7 +107,8 @@ const CSS_DEFAULT_STYLE = css`
100
107
  }
101
108
  `;
102
109
  /**
103
- * CSS class to always include in a top-level div wrapper in the card template to allow for custom styling.
110
+ * CSS class to always include in a top-level div wrapper in the card template
111
+ * to allow for custom styling.
104
112
  */
105
113
  const CSS_DEFAULT_CLASS_NAME = "yanki";
106
114
  /**
@@ -110,12 +118,13 @@ const CSS_DEFAULT_CLASS_NAME = "yanki";
110
118
  */
111
119
  const NOTE_DEFAULT_DECK_NAME = "Yanki";
112
120
  /**
113
- * Text to show if a note 'Front' field is empty, and content is required for a semantically valid card.
121
+ * Text to show if a note 'Front' field is empty, and content is required for a
122
+ * semantically valid card.
114
123
  */
115
124
  const NOTE_DEFAULT_EMPTY_TEXT = "(Empty)";
116
125
  /**
117
- * HTML element to use to present `NOTE_DEFAULT_EMPTY_TEXT`.
118
- * TODO consider hidden span?
126
+ * HTML element to use to present `NOTE_DEFAULT_EMPTY_TEXT`. TODO consider
127
+ * hidden span?
119
128
  */
120
129
  const NOTE_DEFAULT_EMPTY_HAST = u("element", {
121
130
  properties: {},
@@ -130,19 +139,23 @@ const MEDIA_DEFAULT_HASH_MODE_URL = "metadata";
130
139
  * How to first attempt to infer the asset type behind a URL.
131
140
  *
132
141
  * - `metadata`: Fetch the head and hope for a `Content-Type` header.
133
- * - `name`: Infer the extension from the URL alone, won't work if there's nothing extension-like in the `pathname`.
142
+ * - `name`: Infer the extension from the URL alone, won't work if there's nothing
143
+ * extension-like in the `pathname`.
134
144
  */
135
145
  const MEDIA_URL_CONTENT_TYPE_MODE = "metadata";
136
146
  /**
137
- * Filename to use when a media asset has no name. Will be appended with counter parenthetical as needed.
147
+ * Filename to use when a media asset has no name. Will be appended with counter
148
+ * parenthetical as needed.
138
149
  */
139
150
  const MEDIA_DEFAULT_EMPTY_FILENAME = "Untitled";
140
151
  /**
141
152
  * Supported image extensions for Anki media assets.
142
153
  *
143
- * Note that while officially "supported", some of these are not universally compatible across Anki platforms.
154
+ * Note that while officially "supported", some of these are not universally
155
+ * compatible across Anki platforms.
144
156
  *
145
- * Via https://github.com/ankitects/anki/blob/e41c4573d789afe8b020fab5d9d1eede50c3fa3d/qt/aqt/editor.py#L62
157
+ * Via
158
+ * https://github.com/ankitects/anki/blob/e41c4573d789afe8b020fab5d9d1eede50c3fa3d/qt/aqt/editor.py#L62
146
159
  */
147
160
  const MEDIA_SUPPORTED_IMAGE_EXTENSIONS = [
148
161
  "avif",
@@ -360,7 +373,8 @@ const FORBIDDEN_CHARACTERS = [
360
373
  ];
361
374
  /**
362
375
  * Convenience
363
- * @returns sanitized valid namespace
376
+ *
377
+ * @returns Sanitized valid namespace
364
378
  * @throws {Error} If namespace is invalid
365
379
  */
366
380
  function validateAndSanitizeNamespace(namespace, allowAsterisk = false) {
@@ -369,7 +383,8 @@ function validateAndSanitizeNamespace(namespace, allowAsterisk = false) {
369
383
  }
370
384
  /**
371
385
  * Used internally before storing and searching
372
- * @returns sanitized namespace
386
+ *
387
+ * @returns Sanitized namespace
373
388
  */
374
389
  function sanitizeNamespace(namespace) {
375
390
  return namespace.normalize("NFC").trim();
@@ -383,11 +398,13 @@ function sanitizeNamespace(namespace) {
383
398
  * about the letter of the namespace, otherwise there's a risk of data loss. For
384
399
  * this reason, validation is strict and throws errors, so that the user can
385
400
  * understand and correct their input so that they know the proper form for
386
- * subsequent uses of the namespace string — especially if they're using the CLI.
401
+ * subsequent uses of the namespace string — especially if they're using the
402
+ * CLI.
387
403
  *
388
404
  * Silently correcting the namespace would be a bad idea, because the user might
389
405
  * not realize that the namespace has been changed, and then they might not be
390
406
  * able to find their notes.
407
+ *
391
408
  * @throws {Error}
392
409
  */
393
410
  function validateNamespace(namespace, allowAsterisk = false) {
@@ -462,7 +479,9 @@ const WINDOWS_DRIVE_LETTER_REGEX = /^[A-Z]:/i;
462
479
  const QUERY_FRAGMENT_START_REGEX = /[#?^]/;
463
480
  /**
464
481
  * The browserify polyfill doesn't implement win32 absolute path detection...
482
+ *
465
483
  * @param filePath Normalized path
484
+ *
466
485
  * @returns Whether the path is absolute
467
486
  */
468
487
  function isAbsolute(filePath) {
@@ -472,8 +491,10 @@ const RE_WINDOWS_EXTENDED_LENGTH_PATH = /^\\\\\?\\.+/;
472
491
  /**
473
492
  * Converts all paths to cross-platform 'mixed' style with forward slashes.
474
493
  * Warns on unsupported Windows extended length paths.
494
+ *
475
495
  * @param filePath Path to normalize
476
- * @returns normalized path
496
+ *
497
+ * @returns Normalized path
477
498
  */
478
499
  function normalize(filePath) {
479
500
  if (RE_WINDOWS_EXTENDED_LENGTH_PATH.test(filePath)) {
@@ -486,14 +507,15 @@ function normalize(filePath) {
486
507
  return normalizedPath;
487
508
  }
488
509
  /**
489
- * Special handling for `/absolute-path.md` style links in Obsidian
490
- * and static site generators, where absolute paths are relative to a base path
491
- * instead of the volume root.
510
+ * Special handling for `/absolute-path.md` style links in Obsidian and static
511
+ * site generators, where absolute paths are relative to a base path instead of
512
+ * the volume root.
492
513
  *
493
-
514
+ * Paths starting with Windows drive letters, while technically absolute, are
515
+ * _not_ prepended with the base:
494
516
  *
495
- * Paths starting with Windows drive letters, while technically absolute, are _not_ prepended with the base:
496
- * - If no base path is provided, paths are resolved relative to the the provided CWD.
517
+ * - If no base path is provided, paths are resolved relative to the the provided
518
+ * CWD.
497
519
  * - If paths are relative, the base paths are ignored and the CWD is used.
498
520
  *
499
521
  * All path values are normalized and in 'mixed' platform style.
@@ -570,8 +592,9 @@ function isUrl(text) {
570
592
  return safeParseUrl(text) !== void 0;
571
593
  }
572
594
  /**
573
- * Helper to "filter" file URLs into path strings so they're treated
574
- * correctly in mdastToHtml
595
+ * Helper to "filter" file URLs into path strings so they're treated correctly
596
+ * in mdastToHtml
597
+ *
575
598
  * @todo Need stuff from node's implementation, fileURLToPath?
576
599
  */
577
600
  function fileUrlToPath(url) {
@@ -593,9 +616,12 @@ function getSrcType(filePathOrUrl) {
593
616
  }
594
617
  /**
595
618
  * Supports both Header type and Record<string, string> type
619
+ *
596
620
  * @param headers Headers object or record from a fetch response
597
621
  * @param headerKeys Headers to include in the string
598
- * @returns a concatenated string of the header contents, suitable for hashing, or undefined if no matching headers are present
622
+ *
623
+ * @returns A concatenated string of the header contents, suitable for hashing,
624
+ * or undefined if no matching headers are present
599
625
  */
600
626
  function getHeadersString(headers, headerKeys) {
601
627
  if (headers === void 0) return;
@@ -648,11 +674,11 @@ async function getFileExtensionFromUrl(url, fetchAdapter, mode = MEDIA_URL_CONTE
648
674
  *
649
675
  * - `filename`: Use the filename of the media asset, no network required.
650
676
  * - `metadata`: Use the metadata of the media asset, either fstat stuff for
651
- * files, or reading the headers for URLs... requires a network request for
652
- * remote urls. Falls through to `filename` if not available.
653
- * - `content`: Actually read the content of the media asset, requires reading
654
- * the file or fetching the URL. Not yet implemented. Falls through to
655
- * `metadata` if not available.
677
+ * files, or reading the headers for URLs... requires a network request for
678
+ * remote urls. Falls through to `filename` if not available.
679
+ * - `content`: Actually read the content of the media asset, requires reading the
680
+ * file or fetching the URL. Not yet implemented. Falls through to `metadata`
681
+ * if not available.
656
682
  */
657
683
  async function getUrlContentHash(url, fetchAdapter, mode = MEDIA_DEFAULT_HASH_MODE_URL) {
658
684
  switch (mode) {
@@ -687,8 +713,11 @@ function hostAndPortToUrl(host, port) {
687
713
  //#region src/lib/utilities/media.ts
688
714
  /**
689
715
  * Get the extension of a media file, if it's supported
690
- * @returns Extension without the `.`, possibly an extra string if no extension is found
716
+ *
717
+ * @returns Extension without the `.`, possibly an extra string if no extension
718
+ * is found
691
719
  * @todo Check for how it handles query strings
720
+ *
692
721
  * @todo Clean up type casting
693
722
  */
694
723
  async function getAnkiMediaFilenameExtension(pathOrUrl, fetchAdapter) {
@@ -704,8 +733,8 @@ async function mediaAssetExists(absolutePathOrUrl, fileAdapter, fetchAdapter) {
704
733
  return fileExists(absolutePathOrUrl, fileAdapter);
705
734
  }
706
735
  /**
707
- * Get a safe filename for an Anki media asset
708
- * Anki truncates long file names... so we crush the complete path down to a hash
736
+ * Get a safe filename for an Anki media asset Anki truncates long file names...
737
+ * so we crush the complete path down to a hash
709
738
  */
710
739
  async function getSafeAnkiMediaFilename(absolutePathOrUrl, namespace, fileExtension, fileAdapter, fetchAdapter) {
711
740
  if (!await mediaAssetExists(absolutePathOrUrl, fileAdapter, fetchAdapter)) return;
@@ -722,6 +751,10 @@ async function getContentHash(absolutePathOrUrl, fileAdapter, fetchAdapter) {
722
751
  }
723
752
  //#endregion
724
753
  //#region src/lib/parse/rehype-mathjax-anki.ts
754
+ const CONSECUTIVE_BRACES_REGEX = /([{}])(?=[{}])/g;
755
+ function sanitizeBraces(value) {
756
+ return value.replaceAll(CONSECUTIVE_BRACES_REGEX, "$1 ");
757
+ }
725
758
  /**
726
759
  * Non-rendering replacement for the `rehype-mathjax` plugin, which takes output
727
760
  * from `remark-math` and wraps it in Anki-specific syntax.
@@ -740,6 +773,7 @@ const plugin$3 = function() {
740
773
  if (node.tagName === "code" && Array.isArray(node.properties.className) && node.properties.className.includes("language-math")) {
741
774
  const isBlock = node.properties.className.includes("math-display") || fenced;
742
775
  fenced = false;
776
+ for (const child of node.children) if (child.type === "text") child.value = sanitizeBraces(child.value);
743
777
  node.tagName = isBlock ? "div" : "span";
744
778
  node.children = [
745
779
  {
@@ -939,7 +973,9 @@ function parseDimensions(dimensions) {
939
973
  }
940
974
  /**
941
975
  * Determine if a HAST tree is visually empty.
976
+ *
942
977
  * @param tree - The HAST tree to check.
978
+ *
943
979
  * @returns - True if the tree is visually empty, otherwise false.
944
980
  */
945
981
  function isVisuallyEmpty(tree) {
@@ -976,10 +1012,12 @@ function isVisuallyEmpty(tree) {
976
1012
  return !hasVisualContent;
977
1013
  }
978
1014
  /**
979
- * Add a first child to the first div element in a HAST tree.
980
- * Intended for use with the "div-wrapped" HAST tree generated early in `mdastToHtml`.
1015
+ * Add a first child to the first div element in a HAST tree. Intended for use
1016
+ * with the "div-wrapped" HAST tree generated early in `mdastToHtml`.
1017
+ *
981
1018
  * @param tree - The HAST tree to modify in place.
982
1019
  * @param newChild - The new child node to add.
1020
+ *
983
1021
  * @returns - The modified-in-place HAST tree.
984
1022
  */
985
1023
  function addFirstChildToFirstDiv(tree, newChild) {
@@ -1069,9 +1107,12 @@ async function deleteNotes(client, notes, dryRun = false) {
1069
1107
  *
1070
1108
  * Duplicates will be created if present in the source. It's up to the user to
1071
1109
  * manage their Markdown files as they like.
1110
+ *
1072
1111
  * @param client An instance of YankiConnect
1073
1112
  * @param note The note to add
1074
- * @param dryRun If true, the note will not be created and an ID of 0 will be returned
1113
+ * @param dryRun If true, the note will not be created and an ID of 0 will be
1114
+ * returned
1115
+ *
1075
1116
  * @returns The ID of the newly created note in Anki
1076
1117
  * @throws {Error}
1077
1118
  */
@@ -1103,11 +1144,14 @@ async function addNote(client, note, dryRun, fileAdapter) {
1103
1144
  }
1104
1145
  /**
1105
1146
  * Updates a note in Anki.
1147
+ *
1106
1148
  * @param client An instance of YankiConnect
1107
1149
  * @param localNote A note read from a markdown file
1108
1150
  * @param remoteNote A note loaded from Anki
1151
+ *
1109
1152
  * @returns True if the note was updated, false otherwise.
1110
- * @throws {Error} If the local note ID or remote note cards are undefined, or if model/deck errors occur.
1153
+ * @throws {Error} If the local note ID or remote note cards are undefined, or
1154
+ * if model/deck errors occur.
1111
1155
  */
1112
1156
  async function updateNote(client, localNote, remoteNote, dryRun, fileAdapter) {
1113
1157
  if (localNote.noteId === void 0) throw new Error("Local note ID is undefined");
@@ -1146,6 +1190,7 @@ async function updateNote(client, localNote, remoteNote, dryRun, fileAdapter) {
1146
1190
  }
1147
1191
  /**
1148
1192
  * Helper to compare local and remote field contents.
1193
+ *
1149
1194
  * @returns True if the fields are equal, false otherwise.
1150
1195
  */
1151
1196
  function areFieldsEqual(localFields, remoteFields) {
@@ -1167,14 +1212,14 @@ function areNotesEqual(noteA, noteB, includeId = true) {
1167
1212
  return true;
1168
1213
  }
1169
1214
  /**
1170
- * Helper function to compare two arrays of tags.
1171
- * Note some nuances around case insensitivity as discussed here:
1172
- * https://github.com/kitschpatrol/yanki-obsidian/issues/44
1173
- * Anki will alphabetically sort tags, so we sort as well.
1174
- * Duplicate tags are ignored in Anki, so we ignore them here:
1175
- * ['yes', 'yes'] is considered equal to ['yes'].
1176
- * Tags in different orders are considered equal:
1177
- * ['yes', 'no'] is considered equal to ['no', 'yes'].
1215
+ * Helper function to compare two arrays of tags. Note some nuances around case
1216
+ * insensitivity as discussed here:
1217
+ * https://github.com/kitschpatrol/yanki-obsidian/issues/44 Anki will
1218
+ * alphabetically sort tags, so we sort as well. Duplicate tags are ignored in
1219
+ * Anki, so we ignore them here: ['yes', 'yes'] is considered equal to ['yes'].
1220
+ * Tags in different orders are considered equal: ['yes', 'no'] is considered
1221
+ * equal to ['no', 'yes'].
1222
+ *
1178
1223
  * @returns True if the tags are equal, false otherwise.
1179
1224
  */
1180
1225
  function areTagsEqual(localTags, remoteTags) {
@@ -1184,8 +1229,11 @@ function areTagsEqual(localTags, remoteTags) {
1184
1229
  }
1185
1230
  /**
1186
1231
  * Get all notes from Anki that match the model prefix.
1232
+ *
1187
1233
  * @param client An instance of YankiConnect
1188
- * @param namespace The value of the YankiNamespace field, or search with '*' to get all notes. Defaults to the global default namespace.
1234
+ * @param namespace The value of the YankiNamespace field, or search with '*' to
1235
+ * get all notes. Defaults to the global default namespace.
1236
+ *
1189
1237
  * @returns An array of YankiNote objects
1190
1238
  * @throws {Error}
1191
1239
  */
@@ -1196,14 +1244,19 @@ async function getRemoteNotes(client, namespace = defaultGlobalOptions.namespace
1196
1244
  * Get all data from Anki required to populate the YankiNote type.
1197
1245
  *
1198
1246
  * Handles some extra footwork to identify the deck name and validate the model
1199
- * name. There's no way to get everything we need in one shot from Anki-Connect.
1247
+ * name. There's no way to get everything we need in one shot from
1248
+ * Anki-Connect.
1200
1249
  *
1201
1250
  * Undefined elements in the returned array are subsequently used to identify
1202
1251
  * notes that need to be created.
1252
+ *
1203
1253
  * @param client An instance of YankiConnect
1204
1254
  * @param noteIds An array of local note IDs to (attempt) to fetch
1205
- * @returns Array of YankiNote objects, with undefined for notes that could not be found.
1206
- * @throws {Error} If an unknown model name or multiple decks are found for a note, or if no deck is found.
1255
+ *
1256
+ * @returns Array of YankiNote objects, with undefined for notes that could not
1257
+ * be found.
1258
+ * @throws {Error} If an unknown model name or multiple decks are found for a
1259
+ * note, or if no deck is found.
1207
1260
  */
1208
1261
  async function getRemoteNotesById(client, noteIds) {
1209
1262
  const ankiNotes = await client.note.notesInfo({ notes: noteIds });
@@ -1311,7 +1364,8 @@ async function deleteOrphanedDecks(client, activeNotes, originalNotes, dryRun) {
1311
1364
  return decksToDelete;
1312
1365
  }
1313
1366
  /**
1314
- * Global! Does not respect namespace. You can write namespace checks into your css if you want.
1367
+ * Global! Does not respect namespace. You can write namespace checks into your
1368
+ * css if you want.
1315
1369
  */
1316
1370
  async function updateModelStyle(client, modelName, css, dryRun) {
1317
1371
  let currentCss;
@@ -1343,6 +1397,7 @@ async function getModelStyle(client, modelName = yankiModelNames[0]) {
1343
1397
  }
1344
1398
  /**
1345
1399
  * Upload all media files for a note to Anki.
1400
+ *
1346
1401
  * @returns Original source name of media files uploaded
1347
1402
  */
1348
1403
  async function uploadMediaForNote(client, note, dryRun, fileAdapter) {
@@ -1416,7 +1471,9 @@ async function reconcileMedia(client, liveNotes, namespace, dryRun, fileAdapter)
1416
1471
  }
1417
1472
  /**
1418
1473
  * Request permission to access Anki through Anki-Connect.
1419
- * @returns 'ankiUnreachable' if Anki is not open, or 'granted' if everything is copacetic
1474
+ *
1475
+ * @returns 'ankiUnreachable' if Anki is not open, or 'granted' if everything is
1476
+ * copacetic
1420
1477
  * @throws {Error} If access is denied
1421
1478
  */
1422
1479
  async function requestPermission(client) {
@@ -1443,8 +1500,10 @@ const defaultCleanOptions = { ...defaultGlobalOptions };
1443
1500
  * Deletes all remote notes in Anki associated with the given namespace.
1444
1501
  *
1445
1502
  * Use with significant caution. Mostly useful for testing.
1503
+ *
1446
1504
  * @returns The IDs of the notes that were deleted
1447
- * @throws {Error} If Anki is unreachable or another error occurs during deletion.
1505
+ * @throws {Error} If Anki is unreachable or another error occurs during
1506
+ * deletion.
1448
1507
  */
1449
1508
  async function cleanNotes(options) {
1450
1509
  const startTime = performance.now();
@@ -1552,8 +1611,11 @@ function getSafeTitleForNote(note, manageFilenames, maxLength) {
1552
1611
  }
1553
1612
  /**
1554
1613
  * Get a safe filename for a media asset
1614
+ *
1555
1615
  * @param text Text to be converted to a safe filename
1556
- * @param maxLength If undefined, no truncation will take place. If defined, a maximum maximum length of the filename will be enforced.
1616
+ * @param maxLength If undefined, no truncation will take place. If defined, a
1617
+ * maximum maximum length of the filename will be enforced.
1618
+ *
1557
1619
  * @returns A safe filename
1558
1620
  */
1559
1621
  function getSafeFilename(text, maxLength) {
@@ -1582,8 +1644,11 @@ function auditUniqueFilePath(filePath, existingFilenames) {
1582
1644
  }
1583
1645
  /**
1584
1646
  * Strip the trailing increment from a filename
1585
- * @param filename File name with or without an extension, and possibly with a (1)
1586
- * @returns filename without the increment
1647
+ *
1648
+ * @param filename File name with or without an extension, and possibly with a
1649
+ * (1)
1650
+ *
1651
+ * @returns Filename without the increment
1587
1652
  */
1588
1653
  function stripFilenameIncrement(filename) {
1589
1654
  const validExtension = filename.endsWith(".") || filename.endsWith(")") ? void 0 : path.extname(filename);
@@ -1610,27 +1675,31 @@ const defaultResolveLinkOptions = {
1610
1675
  /**
1611
1676
  * Resolve a file path, URL, or wiki-style named links to an absolute path.
1612
1677
  *
1613
- * Warning:
1614
- * Wiki name link resolution is CASE INSENSITIVE, like in Obsidian, though
1615
- * the case of the matching file will be preserved in the returned path.
1678
+ * Warning: Wiki name link resolution is CASE INSENSITIVE, like in Obsidian,
1679
+ * though the case of the matching file will be preserved in the returned path.
1680
+ *
1616
1681
  * @param filePathOrUrl May be one of:
1617
- * - Wiki named link
1618
- * - Relative file path
1619
- * - Bare file path
1620
- * - Absolute file path
1621
- * - HTTP protocol URL string
1622
- * - File protocol URL string
1623
- * - Obsidian protocol URL string
1624
- * (All file paths can be Windows or POSIX, with or without URI encoding, with
1625
- * or without funky Obsidian-style post-extension block and heading anchor
1626
- * additions.)
1682
+ *
1683
+ * - Wiki named link
1684
+ * - Relative file path
1685
+ * - Bare file path
1686
+ * - Absolute file path
1687
+ * - HTTP protocol URL string
1688
+ * - File protocol URL string
1689
+ * - Obsidian protocol URL string (All file paths can be Windows or POSIX, with or
1690
+ * without URI encoding, with or without funky Obsidian-style
1691
+ * post-extension block and heading anchor additions.)
1692
+ *
1693
+ *
1627
1694
  * @returns Resolved absolute path or URL One of:
1628
- * - Resolved absolute POSIX-style paths
1695
+ *
1696
+ * - Resolved absolute POSIX-style paths
1629
1697
  * - Removes any file path query parameters
1630
1698
  * - Not URI-encoded
1631
1699
  * - Retains original case
1632
- * - HTTP protocol URL
1633
- * - Obsidian protocol vault URL (Optionally, this will include file query parameters)
1700
+ * - HTTP protocol URL
1701
+ * - Obsidian protocol vault URL (Optionally, this will include file query
1702
+ * parameters)
1634
1703
  */
1635
1704
  function resolveLink(filePathOrUrl, options) {
1636
1705
  const { allFilePaths, basePath, convertFilePathsToProtocol, cwd, obsidianVaultName, type } = deepmerge(defaultResolveLinkOptions, options ?? {});
@@ -1695,16 +1764,19 @@ function resolveLink(filePathOrUrl, options) {
1695
1764
  *
1696
1765
  * See Obsidian's `getFirstLinkpathDest()` for a roughly equivalent algorithm.
1697
1766
  *
1698
- * Obsidian seems to treat note links slightly differently from image / asset links.
1767
+ * Obsidian seems to treat note links slightly differently from image / asset
1768
+ * links.
1769
+ *
1699
1770
  * @param name Non-URI-encoded name of the file, may have file extension, if no
1700
- * match with a non-.md extension is found, a match will be attempted with .md
1701
- * regardless. (POSIX-style paths.)
1771
+ * match with a non-.md extension is found, a match will be attempted with .md
1772
+ * regardless. (POSIX-style paths.)
1702
1773
  * @param cwd Absolute path to the current working directory of the file from
1703
- * which we're resolving the link. (POSIX-style paths)
1774
+ * which we're resolving the link. (POSIX-style paths)
1704
1775
  * @param allFilePaths Array of absolute paths to all other files in the paths
1705
- * to be considered. (POSIX-style paths.)
1776
+ * to be considered. (POSIX-style paths.)
1777
+ *
1706
1778
  * @returns Absolute path to the best matching file with the name provided, or
1707
- * undefined if there's no valid match. (POSIX-style paths.)
1779
+ * undefined if there's no valid match. (POSIX-style paths.)
1708
1780
  */
1709
1781
  function resolveNameLink(name, cwd, allFilePaths) {
1710
1782
  if (allFilePaths.length === 0) return;
@@ -1729,10 +1801,13 @@ function resolveNameLink(name, cwd, allFilePaths) {
1729
1801
  /**
1730
1802
  * Check for presence of a path in a list in a case- and query- agnostic manner.
1731
1803
  * Ignores .md extensions to simplify matching files
1804
+ *
1732
1805
  * @param filePath File path with file extension. (POSIX-style path.)
1733
- * @param allFilePaths Array of absolute file paths to check. (POSIX-style paths.)
1806
+ * @param allFilePaths Array of absolute file paths to check. (POSIX-style
1807
+ * paths.)
1808
+ *
1734
1809
  * @returns The file path if it is present in the list of all file paths, or
1735
- * undefined if it is not.
1810
+ * undefined if it is not.
1736
1811
  */
1737
1812
  function pathExistsInAllFiles(filePath, allFilePaths) {
1738
1813
  const base = getBase(filePath);
@@ -1941,9 +2016,9 @@ function wikiBasic() {
1941
2016
  return insideLabel;
1942
2017
  }
1943
2018
  /**
1944
- * When encountering a right square bracket, we must look ahead at the next character
1945
- * to determine whether it indicates the end of the [[wikilink]] or is
1946
- * simply part of the label text.
2019
+ * When encountering a right square bracket, we must look ahead at the next
2020
+ * character to determine whether it indicates the end of the [[wikilink]]
2021
+ * or is simply part of the label text.
1947
2022
  */
1948
2023
  function lookaheadClosingMarker(code) {
1949
2024
  if (code !== 93) return nok(code);
@@ -2061,12 +2136,12 @@ function wikiBasic() {
2061
2136
  *
2062
2137
  * Obsidian also supports wiki links in Markdown-style image and link syntax, so
2063
2138
  * handling resolution here would miss those cases, so:
2139
+ *
2064
2140
  * - Resolution of wiki link into absolute paths happens later in
2065
2141
  * remark-resolve-links.ts
2066
2142
  * - Parsing of Obsidian-style image size from alias / alt text happens later in
2067
2143
  * rehype-utilities.ts
2068
2144
  *
2069
- *
2070
2145
  * Note that only wiki links support spaces in the src, regular markdown links
2071
2146
  * MUST be URI-encoded in the Markdown source Here, we URI-encode for
2072
2147
  * consistency with the regular image syntax in the resulting HAST `<>` escaped
@@ -2471,29 +2546,29 @@ async function loadLocalNotes(allLocalFilePaths, options) {
2471
2546
  }
2472
2547
  const defaultDeckNamesFromFilePathsOptions = { mode: "common-root" };
2473
2548
  /**
2474
- * Helper function to infer deck names from file paths if `deckName` not defined in the note's frontmatter.
2549
+ * Helper function to infer deck names from file paths if `deckName` not defined
2550
+ * in the note's frontmatter.
2475
2551
  *
2476
2552
  * `deckName` will always override the inferred deck name.
2477
2553
  *
2478
2554
  * Depends on the context of _all_ file paths passed to `syncNoteFiles`.
2479
2555
  *
2480
- * Example of paths -> deck names with `common-root`:
2481
- * /base/foo/note.md -> foo
2556
+ * Example of paths -> deck names with `common-root`: /base/foo/note.md -> foo
2482
2557
  * /base/foo/baz/note.md -> foo::baz
2483
2558
  *
2484
- * Example of paths -> deck names with `common-root`:
2485
- * /base/foo/note.md -> foo
2559
+ * Example of paths -> deck names with `common-root`: /base/foo/note.md -> foo
2486
2560
  * /base/foo/note.md -> foo
2487
2561
  *
2488
- * Example of paths -> deck names with `common-parent`:
2489
- * /base/foo/note.md -> base::foo
2490
- * /base/foo/baz/note.md -> base::foo::baz
2562
+ * Example of paths -> deck names with `common-parent`: /base/foo/note.md ->
2563
+ * base::foo /base/foo/baz/note.md -> base::foo::baz
2491
2564
  *
2492
- * Example of paths -> deck names with `common-parent`:
2493
- * /base/foo/note.md -> foo
2565
+ * Example of paths -> deck names with `common-parent`: /base/foo/note.md -> foo
2494
2566
  * /base/foo/note.md -> foo
2495
- * @param absoluteFilePaths Absolute paths to all markdown Anki note files. (Ensures proper resolution if path module is polyfilled.)
2496
- * @returns array of ::-delimited deck paths
2567
+ *
2568
+ * @param absoluteFilePaths Absolute paths to all markdown Anki note files.
2569
+ * (Ensures proper resolution if path module is polyfilled.)
2570
+ *
2571
+ * @returns Array of ::-delimited deck paths
2497
2572
  */
2498
2573
  function getDeckNamesFromFilePaths(absoluteFilePaths, options) {
2499
2574
  const { mode } = deepmerge(defaultDeckNamesFromFilePathsOptions, options ?? {});
@@ -2638,13 +2713,14 @@ const NEWLINE_REGEX = /\r?\n/;
2638
2713
  *
2639
2714
  * Used when a noteId is received from Anki after creating a note.
2640
2715
  *
2641
- *
2642
2716
  * String manipulation is ugly, but it ensures that the markdown format is
2643
2717
  * preserved verbatim. Running it through the unified AST and then
2644
2718
  * remarkStringify would possibly change the format.
2719
+ *
2645
2720
  * @param markdown Raw markdown string with frontmatter.
2646
- * @param noteId The value to set the noteId to. If undefined, the noteId will
2647
- * be removed from the frontmatter. (Useful for testing.)
2721
+ * @param noteId The value to set the noteId to. If undefined, the noteId will
2722
+ * be removed from the frontmatter. (Useful for testing.)
2723
+ *
2648
2724
  * @returns Raw markdown string with updated frontmatter.
2649
2725
  */
2650
2726
  async function setNoteIdInFrontmatter(markdown, noteId) {
@@ -2688,9 +2764,11 @@ function getFrontmatterRange(markdown) {
2688
2764
  const defaultSyncNotesOptions = { ...defaultGlobalOptions };
2689
2765
  /**
2690
2766
  * Syncs local notes to Anki.
2767
+ *
2691
2768
  * @param allLocalNotes All the YankiNotes to sync
2769
+ *
2692
2770
  * @returns The synced notes (with new IDs where applicable), plus some stats
2693
- * about the sync
2771
+ * about the sync
2694
2772
  * @throws {Error} For various reasons...
2695
2773
  */
2696
2774
  async function syncNotes(allLocalNotes, options) {
@@ -2819,9 +2897,11 @@ const defaultSyncFilesOptions = {
2819
2897
  *
2820
2898
  * Most importantly, it updates the note IDs in the frontmatter of the local
2821
2899
  * files.
2900
+ *
2822
2901
  * @param allLocalFilePaths Array of paths to the local markdown files
2902
+ *
2823
2903
  * @returns The synced files (with new IDs where applicable), plus some stats
2824
- * about the sync
2904
+ * about the sync
2825
2905
  * @throws {Error} If syncing fails or file operations encounter an error.
2826
2906
  */
2827
2907
  async function syncFiles(allLocalFilePaths, options) {