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/bin/cli.js +915 -94
- package/dist/lib/index.d.ts +39 -11
- package/dist/lib/index.js +180 -100
- package/dist/standalone/index.d.ts +73 -35
- package/dist/standalone/index.js +861 -87
- package/package.json +18 -17
- package/readme.md +10 -39
- package/dist/.DS_Store +0 -0
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
|
|
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
|
|
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
|
|
90
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
|
154
|
+
* Note that while officially "supported", some of these are not universally
|
|
155
|
+
* compatible across Anki platforms.
|
|
144
156
|
*
|
|
145
|
-
* Via
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
491
|
-
*
|
|
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
|
-
*
|
|
496
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
652
|
-
*
|
|
653
|
-
* - `content`: Actually read the content of the media asset, requires reading
|
|
654
|
-
*
|
|
655
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
1172
|
-
* https://github.com/kitschpatrol/yanki-obsidian/issues/44
|
|
1173
|
-
*
|
|
1174
|
-
*
|
|
1175
|
-
* ['yes', '
|
|
1176
|
-
*
|
|
1177
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
1206
|
-
* @
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
1586
|
-
* @
|
|
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
|
-
*
|
|
1615
|
-
*
|
|
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
|
-
*
|
|
1618
|
-
*
|
|
1619
|
-
*
|
|
1620
|
-
*
|
|
1621
|
-
*
|
|
1622
|
-
*
|
|
1623
|
-
*
|
|
1624
|
-
* (All file paths can be Windows or POSIX, with or
|
|
1625
|
-
* or without funky Obsidian-style
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1633
|
-
*
|
|
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
|
|
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
|
-
*
|
|
1701
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1776
|
+
* to be considered. (POSIX-style paths.)
|
|
1777
|
+
*
|
|
1706
1778
|
* @returns Absolute path to the best matching file with the name provided, or
|
|
1707
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
1945
|
-
* to determine whether it indicates the end of the [[wikilink]]
|
|
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
|
|
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
|
-
*
|
|
2496
|
-
* @
|
|
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
|
|
2647
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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) {
|